mirror of
https://github.com/selfxyz/self.git
synced 2026-01-07 22:04:03 -05:00
Merge pull request #1494 from selfxyz/release/staging-2025-12-12
Release to Staging - 2025-12-12
This commit is contained in:
56
.github/actions/generate-github-token/action.yml
vendored
Normal file
56
.github/actions/generate-github-token/action.yml
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
name: "Generate GitHub App Token"
|
||||
description: "Generates a GitHub App token for accessing repositories in the selfxyz organization"
|
||||
|
||||
inputs:
|
||||
app-id:
|
||||
description: "The GitHub App ID"
|
||||
required: true
|
||||
private-key:
|
||||
description: "The GitHub App private key"
|
||||
required: true
|
||||
configure-netrc:
|
||||
description: "If true, writes a ~/.netrc entry for github.com using the generated token (useful for CocoaPods / git HTTPS fetches)"
|
||||
required: false
|
||||
default: "false"
|
||||
netrc-machine:
|
||||
description: "The machine hostname to write into ~/.netrc (default: github.com)"
|
||||
required: false
|
||||
default: "github.com"
|
||||
owner:
|
||||
description: "The owner (organization) of the repositories"
|
||||
required: false
|
||||
default: "selfxyz"
|
||||
repositories:
|
||||
description: "Comma-separated list of repository names to grant access to"
|
||||
required: false
|
||||
default: "NFCPassportReader,android-passport-nfc-reader,react-native-passport-reader,mobile-sdk-native"
|
||||
|
||||
outputs:
|
||||
token:
|
||||
description: "The generated GitHub App installation token"
|
||||
value: ${{ steps.app-token.outputs.token }}
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Generate GitHub App Token
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ inputs.app-id }}
|
||||
private-key: ${{ inputs.private-key }}
|
||||
owner: ${{ inputs.owner }}
|
||||
repositories: ${{ inputs.repositories }}
|
||||
- name: Configure Git auth via ~/.netrc (optional)
|
||||
if: ${{ inputs.configure-netrc == 'true' }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TOKEN="${{ steps.app-token.outputs.token }}"
|
||||
MACHINE="${{ inputs.netrc-machine }}"
|
||||
|
||||
# Mask the token in logs defensively (it shouldn't print, but this protects against future edits).
|
||||
echo "::add-mask::${TOKEN}"
|
||||
|
||||
printf "machine %s\n login x-access-token\n password %s\n" "${MACHINE}" "${TOKEN}" > "${HOME}/.netrc"
|
||||
chmod 600 "${HOME}/.netrc"
|
||||
21
.github/workflows/mobile-bundle-analysis.yml
vendored
21
.github/workflows/mobile-bundle-analysis.yml
vendored
@@ -5,6 +5,7 @@ env:
|
||||
JAVA_VERSION: 17
|
||||
WORKSPACE: ${{ github.workspace }}
|
||||
APP_PATH: ${{ github.workspace }}/app
|
||||
NODE_ENV: "production"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
@@ -57,6 +58,14 @@ jobs:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
- name: Generate token for self repositories
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
|
||||
uses: ./.github/actions/generate-github-token
|
||||
id: github-token
|
||||
with:
|
||||
app-id: ${{ vars.GH_WORKFLOWS_CROSS_ACCESS_ID }}
|
||||
private-key: ${{ secrets.GH_WORKFLOWS_CROSS_ACCESS_KEY }}
|
||||
configure-netrc: "true"
|
||||
- name: Install Mobile Dependencies
|
||||
uses: ./.github/actions/mobile-setup
|
||||
with:
|
||||
@@ -65,7 +74,7 @@ jobs:
|
||||
ruby_version: ${{ env.RUBY_VERSION }}
|
||||
workspace: ${{ env.WORKSPACE }}
|
||||
env:
|
||||
SELFXYZ_INTERNAL_REPO_PAT: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }}
|
||||
SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }}
|
||||
- name: Build dependencies
|
||||
shell: bash
|
||||
run: yarn workspace @selfxyz/common build
|
||||
@@ -113,6 +122,14 @@ jobs:
|
||||
with:
|
||||
path: app/ios/Pods
|
||||
lockfile: app/ios/Podfile.lock
|
||||
- name: Generate token for self repositories
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
|
||||
uses: ./.github/actions/generate-github-token
|
||||
id: github-token
|
||||
with:
|
||||
app-id: ${{ vars.GH_WORKFLOWS_CROSS_ACCESS_ID }}
|
||||
private-key: ${{ secrets.GH_WORKFLOWS_CROSS_ACCESS_KEY }}
|
||||
configure-netrc: "true"
|
||||
- name: Install Mobile Dependencies
|
||||
uses: ./.github/actions/mobile-setup
|
||||
with:
|
||||
@@ -121,7 +138,7 @@ jobs:
|
||||
ruby_version: ${{ env.RUBY_VERSION }}
|
||||
workspace: ${{ env.WORKSPACE }}
|
||||
env:
|
||||
SELFXYZ_INTERNAL_REPO_PAT: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }}
|
||||
SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }}
|
||||
- name: Build dependencies
|
||||
shell: bash
|
||||
run: yarn workspace @selfxyz/common build
|
||||
|
||||
29
.github/workflows/mobile-ci.yml
vendored
29
.github/workflows/mobile-ci.yml
vendored
@@ -35,7 +35,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
build-deps:
|
||||
runs-on: macos-latest-large
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -90,12 +90,9 @@ jobs:
|
||||
- name: Check App Types
|
||||
run: yarn types
|
||||
working-directory: ./app
|
||||
- name: Check license headers
|
||||
run: node scripts/check-license-headers.mjs --check
|
||||
working-directory: ./
|
||||
|
||||
test:
|
||||
runs-on: macos-latest-large
|
||||
runs-on: ubuntu-latest
|
||||
needs: build-deps
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
@@ -190,6 +187,8 @@ jobs:
|
||||
env:
|
||||
# Increase Node.js memory to prevent hermes-parser WASM memory errors
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
# Override production NODE_ENV for tests - React's production build doesn't include testing utilities
|
||||
NODE_ENV: test
|
||||
run: |
|
||||
# Final verification from app directory perspective
|
||||
echo "Final verification before running tests (from app directory)..."
|
||||
@@ -268,6 +267,7 @@ jobs:
|
||||
- name: Cache Ruby gems
|
||||
uses: ./.github/actions/cache-bundler
|
||||
with:
|
||||
# TODO(jcortejoso): Confirm the path of the bundle cache
|
||||
path: app/ios/vendor/bundle
|
||||
lock-file: app/Gemfile.lock
|
||||
cache-version: ${{ env.GH_CACHE_VERSION }}-${{ env.GH_GEMS_CACHE_VERSION }}-ruby${{ env.RUBY_VERSION }}
|
||||
@@ -315,6 +315,14 @@ jobs:
|
||||
bundle config set --local path 'vendor/bundle'
|
||||
bundle install --jobs 4 --retry 3
|
||||
working-directory: ./app
|
||||
- name: Generate token for self repositories
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
|
||||
uses: ./.github/actions/generate-github-token
|
||||
id: github-token
|
||||
with:
|
||||
app-id: ${{ vars.GH_WORKFLOWS_CROSS_ACCESS_ID }}
|
||||
private-key: ${{ secrets.GH_WORKFLOWS_CROSS_ACCESS_KEY }}
|
||||
configure-netrc: "true"
|
||||
- name: Install iOS Dependencies
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
@@ -325,7 +333,7 @@ jobs:
|
||||
cd app/ios
|
||||
bundle exec bash scripts/pod-install-with-cache-fix.sh
|
||||
env:
|
||||
SELFXYZ_INTERNAL_REPO_PAT: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }}
|
||||
SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }}
|
||||
- name: Resolve iOS workspace
|
||||
run: |
|
||||
WORKSPACE_OPEN="ios/OpenPassport.xcworkspace"
|
||||
@@ -470,12 +478,19 @@ jobs:
|
||||
run: |
|
||||
echo "Cache miss for built dependencies. Building now..."
|
||||
yarn workspace @selfxyz/mobile-app run build:deps
|
||||
- name: Generate token for self repositories
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
|
||||
uses: ./.github/actions/generate-github-token
|
||||
id: github-token
|
||||
with:
|
||||
app-id: ${{ vars.GH_WORKFLOWS_CROSS_ACCESS_ID }}
|
||||
private-key: ${{ secrets.GH_WORKFLOWS_CROSS_ACCESS_KEY }}
|
||||
- name: Setup Android private modules
|
||||
run: |
|
||||
cd ${{ env.APP_PATH }}
|
||||
PLATFORM=android node scripts/setup-private-modules.cjs
|
||||
env:
|
||||
SELFXYZ_INTERNAL_REPO_PAT: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }}
|
||||
SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }}
|
||||
CI: true
|
||||
- name: Build Android (with AAPT2 symlink fix)
|
||||
run: yarn android:ci
|
||||
|
||||
26
.github/workflows/mobile-deploy.yml
vendored
26
.github/workflows/mobile-deploy.yml
vendored
@@ -31,6 +31,7 @@ name: Mobile Deploy
|
||||
env:
|
||||
# Build environment versions
|
||||
RUBY_VERSION: 3.2
|
||||
NODE_ENV: "production"
|
||||
JAVA_VERSION: 17
|
||||
ANDROID_API_LEVEL: 35
|
||||
ANDROID_NDK_VERSION: 27.0.12077973
|
||||
@@ -385,6 +386,7 @@ jobs:
|
||||
id: gems-cache
|
||||
uses: ./.github/actions/cache-bundler
|
||||
with:
|
||||
# TODO(jcortejoso): Confirm the path of the bundle cache
|
||||
path: ${{ env.APP_PATH }}/ios/vendor/bundle
|
||||
lock-file: app/Gemfile.lock
|
||||
cache-version: ${{ env.GH_CACHE_VERSION }}-${{ env.GH_GEMS_CACHE_VERSION }}-ruby${{ env.RUBY_VERSION }}
|
||||
@@ -428,6 +430,14 @@ jobs:
|
||||
fi
|
||||
|
||||
echo "✅ Lock files exist"
|
||||
- name: Generate token for self repositories
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
|
||||
uses: ./.github/actions/generate-github-token
|
||||
id: github-token
|
||||
with:
|
||||
app-id: ${{ vars.GH_WORKFLOWS_CROSS_ACCESS_ID }}
|
||||
private-key: ${{ secrets.GH_WORKFLOWS_CROSS_ACCESS_KEY }}
|
||||
configure-netrc: "true"
|
||||
|
||||
- name: Install Mobile Dependencies (main repo)
|
||||
if: inputs.platform != 'android' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
|
||||
@@ -438,7 +448,7 @@ jobs:
|
||||
ruby_version: ${{ env.RUBY_VERSION }}
|
||||
workspace: ${{ env.WORKSPACE }}
|
||||
env:
|
||||
SELFXYZ_INTERNAL_REPO_PAT: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }}
|
||||
SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }}
|
||||
|
||||
- name: Install Mobile Dependencies (forked PRs - no secrets)
|
||||
if: inputs.platform != 'android' && github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true
|
||||
@@ -691,7 +701,7 @@ jobs:
|
||||
IOS_TESTFLIGHT_GROUPS: ${{ secrets.IOS_TESTFLIGHT_GROUPS }}
|
||||
NODE_OPTIONS: "--max-old-space-size=8192"
|
||||
SEGMENT_KEY: ${{ secrets.SEGMENT_KEY }}
|
||||
SELFXYZ_INTERNAL_REPO_PAT: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }}
|
||||
SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }}
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
TURNKEY_AUTH_PROXY_CONFIG_ID: ${{ secrets.TURNKEY_AUTH_PROXY_CONFIG_ID }}
|
||||
TURNKEY_GOOGLE_CLIENT_ID: ${{ secrets.TURNKEY_GOOGLE_CLIENT_ID }}
|
||||
@@ -1046,6 +1056,14 @@ jobs:
|
||||
|
||||
echo "✅ Lock files exist"
|
||||
|
||||
- name: Generate token for self repositories
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
|
||||
uses: ./.github/actions/generate-github-token
|
||||
id: github-token
|
||||
with:
|
||||
app-id: ${{ vars.GH_WORKFLOWS_CROSS_ACCESS_ID }}
|
||||
private-key: ${{ secrets.GH_WORKFLOWS_CROSS_ACCESS_KEY }}
|
||||
|
||||
- name: Install Mobile Dependencies (main repo)
|
||||
if: inputs.platform != 'ios' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
|
||||
uses: ./.github/actions/mobile-setup
|
||||
@@ -1055,7 +1073,7 @@ jobs:
|
||||
ruby_version: ${{ env.RUBY_VERSION }}
|
||||
workspace: ${{ env.WORKSPACE }}
|
||||
env:
|
||||
SELFXYZ_INTERNAL_REPO_PAT: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }}
|
||||
SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }}
|
||||
PLATFORM: ${{ inputs.platform }}
|
||||
|
||||
- name: Install Mobile Dependencies (forked PRs - no secrets)
|
||||
@@ -1112,7 +1130,7 @@ jobs:
|
||||
cd ${{ env.APP_PATH }}
|
||||
PLATFORM=android node scripts/setup-private-modules.cjs
|
||||
env:
|
||||
SELFXYZ_INTERNAL_REPO_PAT: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }}
|
||||
SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }}
|
||||
CI: true
|
||||
|
||||
- name: Build Dependencies (Android)
|
||||
|
||||
30
.github/workflows/mobile-e2e.yml
vendored
30
.github/workflows/mobile-e2e.yml
vendored
@@ -70,6 +70,14 @@ jobs:
|
||||
- name: Toggle Yarn hardened mode for trusted PRs
|
||||
if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false }}
|
||||
run: echo "YARN_ENABLE_HARDENED_MODE=0" >> $GITHUB_ENV
|
||||
- name: Generate token for self repositories
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
|
||||
uses: ./.github/actions/generate-github-token
|
||||
id: github-token
|
||||
with:
|
||||
app-id: ${{ vars.GH_WORKFLOWS_CROSS_ACCESS_ID }}
|
||||
private-key: ${{ secrets.GH_WORKFLOWS_CROSS_ACCESS_KEY }}
|
||||
configure-netrc: "true"
|
||||
- name: Install deps (internal PRs and protected branches)
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -79,7 +87,7 @@ jobs:
|
||||
retry_wait_seconds: 5
|
||||
command: yarn install --immutable --silent
|
||||
env:
|
||||
SELFXYZ_INTERNAL_REPO_PAT: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }}
|
||||
SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }}
|
||||
- name: Install deps (forked PRs - no secrets)
|
||||
if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true }}
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -138,7 +146,7 @@ jobs:
|
||||
cd app
|
||||
PLATFORM=android node scripts/setup-private-modules.cjs
|
||||
env:
|
||||
SELFXYZ_INTERNAL_REPO_PAT: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }}
|
||||
SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }}
|
||||
CI: true
|
||||
- name: Build Android APK
|
||||
run: |
|
||||
@@ -149,6 +157,8 @@ jobs:
|
||||
- name: Clean up Gradle build artifacts
|
||||
uses: ./.github/actions/cleanup-gradle-artifacts
|
||||
- name: Verify APK and android-passport-nfc-reader integration
|
||||
env:
|
||||
SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }}
|
||||
run: |
|
||||
echo "🔍 Verifying build artifacts..."
|
||||
APK_PATH="app/android/app/build/outputs/apk/debug/app-debug.apk"
|
||||
@@ -160,8 +170,8 @@ jobs:
|
||||
echo "📱 APK size: $APK_SIZE bytes"
|
||||
|
||||
# Verify private modules were properly integrated (skip for forks)
|
||||
if [ -z "${SELFXYZ_INTERNAL_REPO_PAT:-}" ]; then
|
||||
echo "🔕 No PAT available — skipping private module verification"
|
||||
if [ -z "${SELFXYZ_APP_TOKEN:-}" ]; then
|
||||
echo "🔕 No SELFXYZ_APP_TOKEN available — skipping private module verification"
|
||||
else
|
||||
# Verify android-passport-nfc-reader
|
||||
if [ -d "app/android/android-passport-nfc-reader" ]; then
|
||||
@@ -263,6 +273,14 @@ jobs:
|
||||
- name: Toggle Yarn hardened mode for trusted PRs
|
||||
if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false }}
|
||||
run: echo "YARN_ENABLE_HARDENED_MODE=0" >> $GITHUB_ENV
|
||||
- name: Generate token for self repositories
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
|
||||
uses: ./.github/actions/generate-github-token
|
||||
id: github-token
|
||||
with:
|
||||
app-id: ${{ vars.GH_WORKFLOWS_CROSS_ACCESS_ID }}
|
||||
private-key: ${{ secrets.GH_WORKFLOWS_CROSS_ACCESS_KEY }}
|
||||
configure-netrc: "true"
|
||||
- name: Install deps (internal PRs and protected branches)
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -272,7 +290,7 @@ jobs:
|
||||
retry_wait_seconds: 5
|
||||
command: yarn install --immutable --silent
|
||||
env:
|
||||
SELFXYZ_INTERNAL_REPO_PAT: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }}
|
||||
SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }}
|
||||
- name: Install deps (forked PRs - no secrets)
|
||||
if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true }}
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -360,7 +378,7 @@ jobs:
|
||||
echo "📦 Installing pods via centralized script…"
|
||||
BUNDLE_GEMFILE=../Gemfile bundle exec bash scripts/pod-install-with-cache-fix.sh
|
||||
env:
|
||||
SELFXYZ_INTERNAL_REPO_PAT: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }}
|
||||
SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }}
|
||||
- name: Setup iOS Simulator
|
||||
run: |
|
||||
echo "Setting up iOS Simulator..."
|
||||
|
||||
36
.github/workflows/mobile-sdk-demo-e2e.yml
vendored
36
.github/workflows/mobile-sdk-demo-e2e.yml
vendored
@@ -59,6 +59,9 @@ jobs:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
- run: corepack enable
|
||||
- run: corepack prepare yarn@4.6.0 --activate
|
||||
- name: Compute .yarnrc.yml hash
|
||||
id: yarnrc-hash
|
||||
uses: ./.github/actions/yarnrc-hash
|
||||
- name: Cache Yarn dependencies
|
||||
uses: ./.github/actions/cache-yarn
|
||||
with:
|
||||
@@ -66,10 +69,17 @@ jobs:
|
||||
.yarn/cache
|
||||
.yarn/install-state.gz
|
||||
.yarn/unplugged
|
||||
cache-version: ${{ env.GH_CACHE_VERSION }}-node-${{ env.NODE_VERSION_SANITIZED }}-${{ hashFiles('.yarnrc.yml') }}
|
||||
cache-version: ${{ env.GH_CACHE_VERSION }}-node-${{ env.NODE_VERSION_SANITIZED }}-${{ steps.yarnrc-hash.outputs.hash }}
|
||||
- name: Toggle Yarn hardened mode for trusted PRs
|
||||
if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false }}
|
||||
run: echo "YARN_ENABLE_HARDENED_MODE=0" >> $GITHUB_ENV
|
||||
- name: Generate token for self repositories
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
|
||||
uses: ./.github/actions/generate-github-token
|
||||
id: github-token
|
||||
with:
|
||||
app-id: ${{ vars.GH_WORKFLOWS_CROSS_ACCESS_ID }}
|
||||
private-key: ${{ secrets.GH_WORKFLOWS_CROSS_ACCESS_KEY }}
|
||||
- name: Install deps (internal PRs and protected branches)
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -79,7 +89,7 @@ jobs:
|
||||
retry_wait_seconds: 5
|
||||
command: yarn install --immutable --silent
|
||||
env:
|
||||
SELFXYZ_INTERNAL_REPO_PAT: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }}
|
||||
SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }}
|
||||
- name: Install deps (forked PRs - no secrets)
|
||||
if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true }}
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -220,6 +230,9 @@ jobs:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
- run: corepack enable
|
||||
- run: corepack prepare yarn@4.6.0 --activate
|
||||
- name: Compute .yarnrc.yml hash
|
||||
id: yarnrc-hash
|
||||
uses: ./.github/actions/yarnrc-hash
|
||||
- name: Cache Yarn dependencies
|
||||
uses: ./.github/actions/cache-yarn
|
||||
with:
|
||||
@@ -227,10 +240,17 @@ jobs:
|
||||
.yarn/cache
|
||||
.yarn/install-state.gz
|
||||
.yarn/unplugged
|
||||
cache-version: ${{ env.GH_CACHE_VERSION }}-node-${{ env.NODE_VERSION_SANITIZED }}-${{ hashFiles('.yarnrc.yml') }}
|
||||
cache-version: ${{ env.GH_CACHE_VERSION }}-node-${{ env.NODE_VERSION_SANITIZED }}-${{ steps.yarnrc-hash.outputs.hash }}
|
||||
- name: Toggle Yarn hardened mode for trusted PRs
|
||||
if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false }}
|
||||
run: echo "YARN_ENABLE_HARDENED_MODE=0" >> $GITHUB_ENV
|
||||
- name: Generate token for self repositories
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
|
||||
uses: ./.github/actions/generate-github-token
|
||||
id: github-token
|
||||
with:
|
||||
app-id: ${{ vars.GH_WORKFLOWS_CROSS_ACCESS_ID }}
|
||||
private-key: ${{ secrets.GH_WORKFLOWS_CROSS_ACCESS_KEY }}
|
||||
- name: Install deps (internal PRs and protected branches)
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -240,7 +260,7 @@ jobs:
|
||||
retry_wait_seconds: 5
|
||||
command: yarn install --immutable --silent
|
||||
env:
|
||||
SELFXYZ_INTERNAL_REPO_PAT: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }}
|
||||
SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }}
|
||||
- name: Install deps (forked PRs - no secrets)
|
||||
if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true }}
|
||||
uses: nick-fields/retry@v3
|
||||
@@ -316,15 +336,15 @@ jobs:
|
||||
max_attempts: 3
|
||||
retry_wait_seconds: 10
|
||||
command: |
|
||||
if [ -n "${SELFXYZ_INTERNAL_REPO_PAT}" ]; then
|
||||
echo "🔑 Using SELFXYZ_INTERNAL_REPO_PAT for private pod access"
|
||||
echo "::add-mask::${SELFXYZ_INTERNAL_REPO_PAT}"
|
||||
if [ -n "${SELFXYZ_APP_TOKEN}" ]; then
|
||||
echo "🔑 Using GitHub App token for private pod access"
|
||||
echo "::add-mask::${SELFXYZ_APP_TOKEN}"
|
||||
fi
|
||||
cd packages/mobile-sdk-demo/ios
|
||||
echo "📦 Installing pods via cache-fix script…"
|
||||
bash scripts/pod-install-with-cache-fix.sh
|
||||
env:
|
||||
SELFXYZ_INTERNAL_REPO_PAT: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }}
|
||||
SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }}
|
||||
GIT_TERMINAL_PROMPT: 0
|
||||
- name: Setup iOS Simulator
|
||||
run: |
|
||||
|
||||
8
.github/workflows/qrcode-sdk-ci.yml
vendored
8
.github/workflows/qrcode-sdk-ci.yml
vendored
@@ -76,6 +76,7 @@ jobs:
|
||||
with:
|
||||
path: |
|
||||
common/dist
|
||||
sdk/sdk-common/dist
|
||||
sdk/qrcode/dist
|
||||
key: qrcode-sdk-build-${{ env.GH_SDK_CACHE_VERSION }}-${{ github.sha }}
|
||||
|
||||
@@ -128,6 +129,7 @@ jobs:
|
||||
with:
|
||||
path: |
|
||||
common/dist
|
||||
sdk/sdk-common/dist
|
||||
sdk/qrcode/dist
|
||||
key: qrcode-sdk-build-${{ env.GH_SDK_CACHE_VERSION }}-${{ github.sha }}
|
||||
fail-on-cache-miss: true
|
||||
@@ -195,6 +197,7 @@ jobs:
|
||||
with:
|
||||
path: |
|
||||
common/dist
|
||||
sdk/sdk-common/dist
|
||||
sdk/qrcode/dist
|
||||
key: qrcode-sdk-build-${{ env.GH_SDK_CACHE_VERSION }}-${{ github.sha }}
|
||||
fail-on-cache-miss: true
|
||||
@@ -203,6 +206,7 @@ jobs:
|
||||
run: |
|
||||
echo "Verifying build artifacts..."
|
||||
ls -la common/dist/
|
||||
ls -la sdk/sdk-common/dist/
|
||||
ls -la sdk/qrcode/dist/
|
||||
echo "✅ Build artifacts verified"
|
||||
|
||||
@@ -255,13 +259,11 @@ jobs:
|
||||
with:
|
||||
path: |
|
||||
common/dist
|
||||
sdk/sdk-common/dist
|
||||
sdk/qrcode/dist
|
||||
key: qrcode-sdk-build-${{ env.GH_SDK_CACHE_VERSION }}-${{ github.sha }}
|
||||
fail-on-cache-miss: true
|
||||
|
||||
- name: Build SDK common dependency
|
||||
run: yarn workspace @selfxyz/sdk-common build
|
||||
|
||||
- name: Run tests
|
||||
run: yarn workspace @selfxyz/qrcode test
|
||||
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -24,3 +24,8 @@ packages/mobile-sdk-alpha/docs/docstrings-report.json
|
||||
|
||||
# Private Android modules (cloned at build time)
|
||||
app/android/android-passport-nfc-reader/
|
||||
|
||||
# Foundry
|
||||
contracts/out/
|
||||
contracts/cache_forge/
|
||||
contracts/broadcast/
|
||||
|
||||
7
.gitmodules
vendored
7
.gitmodules
vendored
@@ -1,3 +1,10 @@
|
||||
[submodule "contracts/lib/openzeppelin-foundry-upgrades"]
|
||||
path = contracts/lib/openzeppelin-foundry-upgrades
|
||||
url = https://github.com/OpenZeppelin/openzeppelin-foundry-upgrades
|
||||
[submodule "contracts/lib/forge-std"]
|
||||
path = contracts/lib/forge-std
|
||||
url = https://github.com/foundry-rs/forge-std
|
||||
|
||||
[submodule "packages/mobile-sdk-alpha/mobile-sdk-native"]
|
||||
path = packages/mobile-sdk-alpha/mobile-sdk-native
|
||||
url = git@github.com:selfxyz/mobile-sdk-native.git
|
||||
|
||||
@@ -245,6 +245,8 @@ module.exports = {
|
||||
],
|
||||
// Allow any types in tests for mocking
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
// Allow test skipping without warnings
|
||||
'jest/no-disabled-tests': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -22,7 +22,7 @@ GEM
|
||||
artifactory (3.0.17)
|
||||
atomos (0.1.3)
|
||||
aws-eventstream (1.4.0)
|
||||
aws-partitions (1.1190.0)
|
||||
aws-partitions (1.1194.0)
|
||||
aws-sdk-core (3.239.2)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
@@ -86,7 +86,7 @@ GEM
|
||||
colored2 (3.1.2)
|
||||
commander (4.6.0)
|
||||
highline (~> 2.0.0)
|
||||
concurrent-ruby (1.3.5)
|
||||
concurrent-ruby (1.3.6)
|
||||
connection_pool (3.0.2)
|
||||
declarative (0.0.20)
|
||||
digest-crc (0.7.0)
|
||||
@@ -222,14 +222,14 @@ GEM
|
||||
i18n (1.14.7)
|
||||
concurrent-ruby (~> 1.0)
|
||||
jmespath (1.6.2)
|
||||
json (2.17.1)
|
||||
json (2.18.0)
|
||||
jwt (2.10.2)
|
||||
base64
|
||||
logger (1.7.0)
|
||||
mini_magick (4.13.2)
|
||||
mini_mime (1.1.5)
|
||||
mini_portile2 (2.8.9)
|
||||
minitest (5.26.2)
|
||||
minitest (5.27.0)
|
||||
molinillo (0.8.0)
|
||||
multi_json (1.18.0)
|
||||
multipart-post (2.4.1)
|
||||
@@ -241,7 +241,7 @@ GEM
|
||||
nokogiri (1.18.10)
|
||||
mini_portile2 (~> 2.8.2)
|
||||
racc (~> 1.4)
|
||||
optparse (0.8.0)
|
||||
optparse (0.8.1)
|
||||
os (1.1.4)
|
||||
plist (3.7.2)
|
||||
public_suffix (4.0.7)
|
||||
|
||||
@@ -26,4 +26,9 @@ module.exports = {
|
||||
},
|
||||
],
|
||||
],
|
||||
env: {
|
||||
production: {
|
||||
plugins: ['transform-remove-console'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -42,8 +42,15 @@
|
||||
<string></string>
|
||||
<key>LSApplicationQueriesSchemes</key>
|
||||
<array>
|
||||
<string>whatsapp</string>
|
||||
<string>argent</string>
|
||||
<string>cbwallet</string>
|
||||
<string>coinbase</string>
|
||||
<string>metamask</string>
|
||||
<string>rainbow</string>
|
||||
<string>sms</string>
|
||||
<string>trust</string>
|
||||
<string>wc</string>
|
||||
<string>whatsapp</string>
|
||||
</array>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
|
||||
@@ -33,7 +33,7 @@ def using_https_git_auth?
|
||||
auth_data.include?("Logged in to github.com account") &&
|
||||
auth_data.include?("Git operations protocol: https")
|
||||
rescue => e
|
||||
puts "gh auth status failed, assuming no HTTPS auth -- will try SSH"
|
||||
# Avoid printing auth-related details in CI logs.
|
||||
false
|
||||
end
|
||||
end
|
||||
@@ -51,18 +51,16 @@ target "Self" do
|
||||
# External fork - use public NFCPassportReader repository (placeholder)
|
||||
# TODO: Replace with actual public NFCPassportReader repository URL
|
||||
nfc_repo_url = "https://github.com/PLACEHOLDER/NFCPassportReader.git"
|
||||
puts "📦 Using public NFCPassportReader for external fork (#{ENV["GITHUB_REPOSITORY"]})"
|
||||
elsif ENV["GITHUB_ACTIONS"] == "true" && ENV["SELFXYZ_INTERNAL_REPO_PAT"]
|
||||
# Running in selfxyz GitHub Actions with PAT available - use private repo with token
|
||||
nfc_repo_url = "https://#{ENV["SELFXYZ_INTERNAL_REPO_PAT"]}@github.com/selfxyz/NFCPassportReader.git"
|
||||
puts "📦 Using private NFCPassportReader with PAT (selfxyz GitHub Actions)"
|
||||
elsif ENV["GITHUB_ACTIONS"] == "true"
|
||||
# CI: NEVER embed credentials in URLs. Rely on workflow-provided auth via:
|
||||
# - ~/.netrc or a Git credential helper, and token masking in logs.
|
||||
nfc_repo_url = "https://github.com/selfxyz/NFCPassportReader.git"
|
||||
elsif using_https_git_auth?
|
||||
# Local development with HTTPS GitHub auth via gh - use HTTPS to private repo
|
||||
nfc_repo_url = "https://github.com/selfxyz/NFCPassportReader.git"
|
||||
else
|
||||
# Local development in selfxyz repo - use SSH to private repo
|
||||
nfc_repo_url = "git@github.com:selfxyz/NFCPassportReader.git"
|
||||
puts "📦 Using SSH for private NFCPassportReader (local selfxyz development)"
|
||||
end
|
||||
|
||||
pod "NFCPassportReader", git: nfc_repo_url, commit: "9eff7c4e3a9037fdc1e03301584e0d5dcf14d76b"
|
||||
|
||||
@@ -2644,6 +2644,6 @@ SPEC CHECKSUMS:
|
||||
SwiftyTesseract: 1f3d96668ae92dc2208d9842c8a59bea9fad2cbb
|
||||
Yoga: 1259c7a8cbaccf7b4c3ddf8ee36ca11be9dee407
|
||||
|
||||
PODFILE CHECKSUM: b5f11f935be22fce84c5395aaa203b50427a79aa
|
||||
PODFILE CHECKSUM: 0aa47f53692543349c43673cda7380fa23049eba
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
|
||||
@@ -16,7 +16,7 @@ module.exports = {
|
||||
'node',
|
||||
],
|
||||
transformIgnorePatterns: [
|
||||
'node_modules/(?!(react-native|@react-native|@react-navigation|@react-native-community|@segment/analytics-react-native|@openpassport|react-native-keychain|react-native-check-version|react-native-nfc-manager|react-native-passport-reader|react-native-gesture-handler|uuid|@stablelib|@react-native-google-signin|react-native-cloud-storage|@react-native-clipboard|@react-native-firebase|@selfxyz|@sentry|@anon-aadhaar|react-native-svg|react-native-svg-circle-country-flags)/)',
|
||||
'node_modules/(?!(react-native|@react-native|@react-navigation|@react-native-community|@segment/analytics-react-native|@openpassport|react-native-keychain|react-native-check-version|react-native-nfc-manager|react-native-passport-reader|react-native-gesture-handler|uuid|@stablelib|@react-native-google-signin|react-native-cloud-storage|@react-native-clipboard|@react-native-firebase|@selfxyz|@sentry|@anon-aadhaar|react-native-svg|react-native-svg-circle-country-flags|react-native-blur-effect)/)',
|
||||
],
|
||||
setupFiles: ['<rootDir>/jest.setup.js'],
|
||||
testMatch: [
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
"sync-versions": "bundle exec fastlane ios sync_version && bundle exec fastlane android sync_version",
|
||||
"tag:release": "node scripts/tag.cjs release",
|
||||
"tag:remove": "node scripts/tag.cjs remove",
|
||||
"test": "yarn build:deps && yarn jest:run --passWithNoTests && node --test scripts/tests/*.cjs",
|
||||
"test": "yarn jest:run --passWithNoTests && node --test scripts/tests/*.cjs",
|
||||
"test:build": "yarn build:deps && yarn types && node ./scripts/bundle-analyze-ci.cjs ios && yarn test",
|
||||
"test:ci": "yarn jest:run --passWithNoTests && node --test scripts/tests/*.cjs",
|
||||
"test:coverage": "yarn jest:run --coverage --passWithNoTests",
|
||||
@@ -105,6 +105,7 @@
|
||||
"@segment/analytics-react-native": "^2.21.2",
|
||||
"@segment/sovran-react-native": "^1.1.3",
|
||||
"@selfxyz/common": "workspace:^",
|
||||
"@selfxyz/euclid": "^0.6.0",
|
||||
"@selfxyz/mobile-sdk-alpha": "workspace:^",
|
||||
"@sentry/react": "^9.32.0",
|
||||
"@sentry/react-native": "7.0.1",
|
||||
@@ -209,6 +210,7 @@
|
||||
"@typescript-eslint/parser": "^8.39.0",
|
||||
"@vitejs/plugin-react-swc": "^3.10.2",
|
||||
"babel-plugin-module-resolver": "^5.0.2",
|
||||
"babel-plugin-transform-remove-console": "^6.9.4",
|
||||
"constants-browserify": "^1.0.0",
|
||||
"dompurify": "^3.2.6",
|
||||
"eslint": "^8.57.0",
|
||||
|
||||
@@ -17,8 +17,8 @@ if (!platform || !['android', 'ios'].includes(platform)) {
|
||||
// Bundle size thresholds in MB - easy to update!
|
||||
const BUNDLE_THRESHOLDS_MB = {
|
||||
// TODO: fix temporary bundle bump
|
||||
ios: 44,
|
||||
android: 44,
|
||||
ios: 45,
|
||||
android: 45,
|
||||
};
|
||||
|
||||
function formatBytes(bytes) {
|
||||
|
||||
@@ -109,7 +109,13 @@ clone_private_module() {
|
||||
local dir_name=$(basename "$target_dir")
|
||||
|
||||
# Use different clone methods based on environment
|
||||
if is_ci && [[ -n "${SELFXYZ_INTERNAL_REPO_PAT:-}" ]]; then
|
||||
if is_ci && [[ -n "${SELFXYZ_APP_TOKEN:-}" ]]; then
|
||||
# CI environment with GitHub App installation token
|
||||
git clone "https://x-access-token:${SELFXYZ_APP_TOKEN}@github.com/selfxyz/${repo_name}.git" "$dir_name" || {
|
||||
log "ERROR: Failed to clone $repo_name with GitHub App token"
|
||||
exit 1
|
||||
}
|
||||
elif is_ci && [[ -n "${SELFXYZ_INTERNAL_REPO_PAT:-}" ]]; then
|
||||
# CI environment with PAT (fallback if action didn't run)
|
||||
git clone "https://${SELFXYZ_INTERNAL_REPO_PAT}@github.com/selfxyz/${repo_name}.git" "$dir_name" || {
|
||||
log "ERROR: Failed to clone $repo_name with PAT"
|
||||
@@ -119,14 +125,14 @@ clone_private_module() {
|
||||
# Local development with SSH
|
||||
git clone "git@github.com:selfxyz/${repo_name}.git" "$dir_name" || {
|
||||
log "ERROR: Failed to clone $repo_name with SSH"
|
||||
log "Please ensure you have SSH access to the repository or set SELFXYZ_INTERNAL_REPO_PAT"
|
||||
log "Please ensure you have SSH access to the repository or set SELFXYZ_APP_TOKEN/SELFXYZ_INTERNAL_REPO_PAT"
|
||||
exit 1
|
||||
}
|
||||
else
|
||||
log "ERROR: No authentication method available for cloning $repo_name"
|
||||
log "Please either:"
|
||||
log " - Set up SSH access (for local development)"
|
||||
log " - Set SELFXYZ_INTERNAL_REPO_PAT environment variable (for CI)"
|
||||
log " - Set SELFXYZ_APP_TOKEN or SELFXYZ_INTERNAL_REPO_PAT environment variable (for CI)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -194,14 +200,15 @@ log "✅ Package files backed up successfully"
|
||||
# Install SDK from tarball in app with timeout
|
||||
log "Installing SDK as real files..."
|
||||
if is_ci; then
|
||||
# Temporarily unset PAT to skip private modules during SDK installation
|
||||
env -u SELFXYZ_INTERNAL_REPO_PAT timeout 180 yarn add "@selfxyz/mobile-sdk-alpha@file:$TARBALL_PATH" || {
|
||||
# Temporarily unset both auth tokens to skip private modules during SDK installation
|
||||
# Both tokens must be unset to prevent setup-private-modules.cjs from attempting clones
|
||||
env -u SELFXYZ_INTERNAL_REPO_PAT -u SELFXYZ_APP_TOKEN timeout 180 yarn add "@selfxyz/mobile-sdk-alpha@file:$TARBALL_PATH" || {
|
||||
log "SDK installation timed out after 3 minutes"
|
||||
exit 1
|
||||
}
|
||||
else
|
||||
# Temporarily unset PAT to skip private modules during SDK installation
|
||||
env -u SELFXYZ_INTERNAL_REPO_PAT yarn add "@selfxyz/mobile-sdk-alpha@file:$TARBALL_PATH"
|
||||
# Temporarily unset both auth tokens to skip private modules during SDK installation
|
||||
env -u SELFXYZ_INTERNAL_REPO_PAT -u SELFXYZ_APP_TOKEN yarn add "@selfxyz/mobile-sdk-alpha@file:$TARBALL_PATH"
|
||||
fi
|
||||
|
||||
# Verify installation (check for AAR file in both local and hoisted locations)
|
||||
|
||||
@@ -29,8 +29,9 @@ const PRIVATE_MODULES = [
|
||||
|
||||
// Environment detection
|
||||
// CI is set by GitHub Actions, CircleCI, etc. Check for truthy value
|
||||
const isCI = !!process.env.CI || process.env.GITHUB_ACTIONS === 'true';
|
||||
const isCI = process.env.CI === 'true' || process.env.GITHUB_ACTIONS === 'true';
|
||||
const repoToken = process.env.SELFXYZ_INTERNAL_REPO_PAT;
|
||||
const appToken = process.env.SELFXYZ_APP_TOKEN; // GitHub App installation token
|
||||
const isDryRun = process.env.DRY_RUN === 'true';
|
||||
|
||||
// Platform detection for Android-specific modules
|
||||
@@ -150,13 +151,17 @@ function clonePrivateRepo(repoName, localPath) {
|
||||
|
||||
let cloneUrl;
|
||||
|
||||
if (isCI && repoToken) {
|
||||
if (isCI && appToken) {
|
||||
// CI environment with GitHub App installation token
|
||||
log('CI detected: Using SELFXYZ_APP_TOKEN for clone', 'info');
|
||||
cloneUrl = `https://x-access-token:${appToken}@github.com/${GITHUB_ORG}/${repoName}.git`;
|
||||
} else if (isCI && repoToken) {
|
||||
// CI environment with Personal Access Token
|
||||
log('CI detected: Using SELFXYZ_INTERNAL_REPO_PAT for clone', 'info');
|
||||
cloneUrl = `https://${repoToken}@github.com/${GITHUB_ORG}/${repoName}.git`;
|
||||
} else if (isCI) {
|
||||
log(
|
||||
'CI environment detected but SELFXYZ_INTERNAL_REPO_PAT not available - skipping private module setup',
|
||||
'CI environment detected but no token available - skipping private module setup',
|
||||
'info',
|
||||
);
|
||||
log(
|
||||
@@ -173,7 +178,7 @@ function clonePrivateRepo(repoName, localPath) {
|
||||
}
|
||||
|
||||
// Security: Use quiet mode for credentialed URLs to prevent token exposure
|
||||
const isCredentialedUrl = isCI && repoToken;
|
||||
const isCredentialedUrl = isCI && (appToken || repoToken);
|
||||
const quietFlag = isCredentialedUrl ? '--quiet' : '';
|
||||
const targetDir = path.basename(localPath);
|
||||
const cloneCommand = `git clone --branch ${BRANCH} --single-branch --depth 1 ${quietFlag} "${cloneUrl}" "${targetDir}"`;
|
||||
@@ -190,7 +195,7 @@ function clonePrivateRepo(repoName, localPath) {
|
||||
} catch (error) {
|
||||
if (isCI) {
|
||||
log(
|
||||
'Clone failed in CI environment. Check SELFXYZ_INTERNAL_REPO_PAT permissions.',
|
||||
'Clone failed in CI environment. Check SELFXYZ_APP_TOKEN or SELFXYZ_INTERNAL_REPO_PAT permissions.',
|
||||
'error',
|
||||
);
|
||||
} else {
|
||||
@@ -231,7 +236,7 @@ function setupPrivateModule(module) {
|
||||
}
|
||||
|
||||
// Security: Remove credential-embedded remote URL after clone
|
||||
if (isCI && repoToken && !isDryRun) {
|
||||
if (isCI && (appToken || repoToken) && !isDryRun) {
|
||||
scrubGitRemoteUrl(localPath, repoName);
|
||||
}
|
||||
|
||||
@@ -275,6 +280,11 @@ function setupAndroidPassportReader() {
|
||||
`Setup complete: ${successCount}/${PRIVATE_MODULES.length} modules cloned`,
|
||||
'warning',
|
||||
);
|
||||
} else {
|
||||
log(
|
||||
'No private modules were cloned - this is expected for forked PRs',
|
||||
'info',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,11 +6,7 @@ import React from 'react';
|
||||
import { ArrowLeft, ArrowRight, RotateCcw } from '@tamagui/lucide-icons';
|
||||
|
||||
import { Button, XStack, YStack } from '@selfxyz/mobile-sdk-alpha/components';
|
||||
import {
|
||||
black,
|
||||
slate50,
|
||||
slate400,
|
||||
} from '@selfxyz/mobile-sdk-alpha/constants/colors';
|
||||
import { black, slate400 } from '@selfxyz/mobile-sdk-alpha/constants/colors';
|
||||
|
||||
import { buttonTap } from '@/integrations/haptics';
|
||||
|
||||
@@ -23,8 +19,7 @@ export interface WebViewFooterProps {
|
||||
onOpenInBrowser: () => void;
|
||||
}
|
||||
|
||||
const iconSize = 22;
|
||||
const buttonSize = 36;
|
||||
const iconSize = 24;
|
||||
|
||||
export const WebViewFooter: React.FC<WebViewFooterProps> = ({
|
||||
canGoBack,
|
||||
@@ -42,19 +37,13 @@ export const WebViewFooter: React.FC<WebViewFooterProps> = ({
|
||||
) => (
|
||||
<Button
|
||||
key={key}
|
||||
size="$4"
|
||||
unstyled
|
||||
disabled={disabled}
|
||||
hitSlop={{ top: 20, bottom: 20, left: 20, right: 20 }}
|
||||
onPress={() => {
|
||||
buttonTap();
|
||||
onPress();
|
||||
}}
|
||||
backgroundColor={slate50}
|
||||
borderRadius={buttonSize / 2}
|
||||
width={buttonSize}
|
||||
height={buttonSize}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
opacity={disabled ? 0.5 : 1}
|
||||
>
|
||||
{icon}
|
||||
@@ -62,7 +51,7 @@ export const WebViewFooter: React.FC<WebViewFooterProps> = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<YStack gap={12} paddingVertical={12} width="100%">
|
||||
<YStack gap={4} paddingVertical={4} paddingHorizontal={5} width="100%">
|
||||
<XStack justifyContent="space-between" alignItems="center" width="100%">
|
||||
{renderIconButton(
|
||||
'back',
|
||||
|
||||
17
app/src/components/navbar/HeadlessNavForEuclid.tsx
Normal file
17
app/src/components/navbar/HeadlessNavForEuclid.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import { SystemBars } from 'react-native-edge-to-edge';
|
||||
import type { NativeStackHeaderProps } from '@react-navigation/native-stack';
|
||||
|
||||
export const HeadlessNavForEuclid = (props: NativeStackHeaderProps) => {
|
||||
return (
|
||||
<>
|
||||
<SystemBars
|
||||
style={props.options.statusBarStyle}
|
||||
hidden={props.options.statusBarHidden}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -30,9 +30,9 @@ export const WebViewNavBar: React.FC<WebViewNavBarProps> = ({
|
||||
|
||||
return (
|
||||
<XStack
|
||||
paddingHorizontal={20}
|
||||
paddingVertical={10}
|
||||
paddingTop={insets.top + 10}
|
||||
paddingHorizontal={16}
|
||||
gap={14}
|
||||
alignItems="center"
|
||||
backgroundColor="white"
|
||||
@@ -50,7 +50,12 @@ export const WebViewNavBar: React.FC<WebViewNavBarProps> = ({
|
||||
/>
|
||||
|
||||
{/* Center: Title */}
|
||||
<XStack flex={1} alignItems="center" justifyContent="center">
|
||||
<XStack
|
||||
flex={1}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
paddingHorizontal={8}
|
||||
>
|
||||
<Text style={styles.title} numberOfLines={1}>
|
||||
{title?.toUpperCase() || 'PAGE TITLE'}
|
||||
</Text>
|
||||
|
||||
16
app/src/hooks/useSafeAreaInsets.ts
Normal file
16
app/src/hooks/useSafeAreaInsets.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import { Platform } from 'react-native';
|
||||
import { useSafeAreaInsets as useSafeAreaInsetsOriginal } from 'react-native-safe-area-context';
|
||||
|
||||
// gives bare minimums in case safe area doesnt provide for example space for status bar icons.
|
||||
export function useSafeAreaInsets() {
|
||||
const insets = useSafeAreaInsetsOriginal();
|
||||
const minimum = Platform.select({ ios: 54, android: 26, web: 48 });
|
||||
return {
|
||||
...insets,
|
||||
top: Math.max(insets.top, minimum || 0),
|
||||
};
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
white,
|
||||
} from '@selfxyz/mobile-sdk-alpha/constants/colors';
|
||||
|
||||
import { HeadlessNavForEuclid } from '@/components/navbar/HeadlessNavForEuclid';
|
||||
import AccountRecoveryChoiceScreen from '@/screens/account/recovery/AccountRecoveryChoiceScreen';
|
||||
import AccountRecoveryScreen from '@/screens/account/recovery/AccountRecoveryScreen';
|
||||
import DocumentDataNotFoundScreen from '@/screens/account/recovery/DocumentDataNotFoundScreen';
|
||||
@@ -17,6 +18,7 @@ import RecoverWithPhraseScreen from '@/screens/account/recovery/RecoverWithPhras
|
||||
import CloudBackupScreen from '@/screens/account/settings/CloudBackupScreen';
|
||||
import SettingsScreen from '@/screens/account/settings/SettingsScreen';
|
||||
import ShowRecoveryPhraseScreen from '@/screens/account/settings/ShowRecoveryPhraseScreen';
|
||||
import { IS_EUCLID_ENABLED } from '@/utils/devUtils';
|
||||
|
||||
const accountScreens = {
|
||||
AccountRecovery: {
|
||||
@@ -79,14 +81,22 @@ const accountScreens = {
|
||||
screens: {},
|
||||
},
|
||||
},
|
||||
|
||||
ShowRecoveryPhrase: {
|
||||
screen: ShowRecoveryPhraseScreen,
|
||||
options: {
|
||||
title: 'Recovery Phrase',
|
||||
headerStyle: {
|
||||
backgroundColor: white,
|
||||
},
|
||||
} as NativeStackNavigationOptions,
|
||||
options: IS_EUCLID_ENABLED
|
||||
? ({
|
||||
headerShown: true,
|
||||
header: HeadlessNavForEuclid,
|
||||
statusBarStyle: ShowRecoveryPhraseScreen.statusBarStyle,
|
||||
statusBarHidden: ShowRecoveryPhraseScreen.statusBarHidden,
|
||||
} as NativeStackNavigationOptions)
|
||||
: ({
|
||||
title: 'Recovery Phrase',
|
||||
headerStyle: {
|
||||
backgroundColor: white,
|
||||
},
|
||||
} as NativeStackNavigationOptions),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { NativeStackNavigationOptions } from '@react-navigation/native-stac
|
||||
import { black, white } from '@selfxyz/mobile-sdk-alpha/constants/colors';
|
||||
|
||||
import { AadhaarNavBar, IdDetailsNavBar } from '@/components/navbar';
|
||||
import { HeadlessNavForEuclid } from '@/components/navbar/HeadlessNavForEuclid';
|
||||
import AadhaarUploadedSuccessScreen from '@/screens/documents/aadhaar/AadhaarUploadedSuccessScreen';
|
||||
import AadhaarUploadErrorScreen from '@/screens/documents/aadhaar/AadhaarUploadErrorScreen';
|
||||
import AadhaarUploadScreen from '@/screens/documents/aadhaar/AadhaarUploadScreen';
|
||||
@@ -76,7 +77,10 @@ const documentsScreens = {
|
||||
CountryPicker: {
|
||||
screen: CountryPickerScreen,
|
||||
options: {
|
||||
headerShown: false,
|
||||
header: HeadlessNavForEuclid,
|
||||
statusBarHidden: CountryPickerScreen.statusBar?.hidden,
|
||||
statusBarStyle: CountryPickerScreen.statusBar?.style,
|
||||
headerShown: true,
|
||||
} as NativeStackNavigationOptions,
|
||||
},
|
||||
IDPicker: {
|
||||
|
||||
@@ -2,21 +2,85 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import React, { useCallback, useEffect, useRef } from 'react';
|
||||
import Clipboard from '@react-native-clipboard/clipboard';
|
||||
|
||||
import type { RecoveryPhraseVariant } from '@selfxyz/euclid';
|
||||
import { RecoveryPhraseScreen } from '@selfxyz/euclid';
|
||||
import { useSelfClient } from '@selfxyz/mobile-sdk-alpha';
|
||||
import { Description } from '@selfxyz/mobile-sdk-alpha/components';
|
||||
|
||||
import Mnemonic from '@/components/Mnemonic';
|
||||
import useMnemonic from '@/hooks/useMnemonic';
|
||||
import { useSafeAreaInsets } from '@/hooks/useSafeAreaInsets';
|
||||
import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout';
|
||||
import { useSettingStore } from '@/stores/settingStore';
|
||||
import { IS_EUCLID_ENABLED } from '@/utils/devUtils';
|
||||
|
||||
const ShowRecoveryPhraseScreen: React.FC = () => {
|
||||
function useCopyRecoveryPhrase(mnemonic: string[] | undefined) {
|
||||
const [copied, setCopied] = React.useState(false);
|
||||
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const onCopy = useCallback(() => {
|
||||
if (!mnemonic) return;
|
||||
Clipboard.setString(mnemonic.join(' '));
|
||||
setCopied(true);
|
||||
|
||||
// Clear any existing timeout
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
|
||||
// Set new timeout and store its ID
|
||||
timeoutRef.current = setTimeout(() => setCopied(false), 2500);
|
||||
}, [mnemonic]);
|
||||
|
||||
// Cleanup timeout on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return { copied, onCopy };
|
||||
}
|
||||
|
||||
const ShowRecoveryPhraseScreen: React.FC & {
|
||||
statusBarStyle: string;
|
||||
statusBarHidden: boolean;
|
||||
} = () => {
|
||||
const { mnemonic, loadMnemonic } = useMnemonic();
|
||||
const self = useSelfClient();
|
||||
const { copied, onCopy } = useCopyRecoveryPhrase(mnemonic);
|
||||
const { setHasViewedRecoveryPhrase } = useSettingStore();
|
||||
|
||||
const onRevealWords = useCallback(async () => {
|
||||
const onReveal = useCallback(async () => {
|
||||
await loadMnemonic();
|
||||
}, [loadMnemonic]);
|
||||
setHasViewedRecoveryPhrase(true);
|
||||
}, [loadMnemonic, setHasViewedRecoveryPhrase]);
|
||||
|
||||
const insets = useSafeAreaInsets();
|
||||
if (IS_EUCLID_ENABLED) {
|
||||
const variant: RecoveryPhraseVariant = !mnemonic
|
||||
? 'hidden'
|
||||
: copied
|
||||
? 'copied'
|
||||
: 'revealed';
|
||||
return (
|
||||
<>
|
||||
<RecoveryPhraseScreen
|
||||
insets={insets}
|
||||
onReveal={onReveal}
|
||||
words={mnemonic}
|
||||
onBack={self.goBack}
|
||||
variant={variant}
|
||||
onCopy={onCopy}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<ExpandableBottomLayout.Layout backgroundColor="white">
|
||||
<ExpandableBottomLayout.BottomSection
|
||||
@@ -24,7 +88,7 @@ const ShowRecoveryPhraseScreen: React.FC = () => {
|
||||
justifyContent="center"
|
||||
gap={20}
|
||||
>
|
||||
<Mnemonic words={mnemonic} onRevealWords={onRevealWords} />
|
||||
<Mnemonic words={mnemonic} onRevealWords={loadMnemonic} />
|
||||
<Description>
|
||||
This phrase is the only way to recover your account. Keep it secret,
|
||||
keep it safe.
|
||||
@@ -35,3 +99,7 @@ const ShowRecoveryPhraseScreen: React.FC = () => {
|
||||
};
|
||||
|
||||
export default ShowRecoveryPhraseScreen;
|
||||
|
||||
ShowRecoveryPhraseScreen.statusBarHidden =
|
||||
RecoveryPhraseScreen.statusBar.hidden;
|
||||
ShowRecoveryPhraseScreen.statusBarStyle = RecoveryPhraseScreen.statusBar.style;
|
||||
|
||||
@@ -2,8 +2,21 @@
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import type React from 'react';
|
||||
|
||||
import SDKCountryPickerScreen from '@selfxyz/mobile-sdk-alpha/onboarding/country-picker-screen';
|
||||
|
||||
export default function CountryPickerScreen() {
|
||||
return <SDKCountryPickerScreen />;
|
||||
}
|
||||
import { useSafeAreaInsets } from '@/hooks/useSafeAreaInsets';
|
||||
|
||||
type CountryPickerScreenComponent = React.FC & {
|
||||
statusBar: typeof SDKCountryPickerScreen.statusBar;
|
||||
};
|
||||
|
||||
const CountryPickerScreen: CountryPickerScreenComponent = () => {
|
||||
const insets = useSafeAreaInsets();
|
||||
return <SDKCountryPickerScreen insets={insets} />;
|
||||
};
|
||||
|
||||
CountryPickerScreen.statusBar = SDKCountryPickerScreen.statusBar;
|
||||
|
||||
export default CountryPickerScreen;
|
||||
|
||||
@@ -5,8 +5,10 @@
|
||||
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Alert,
|
||||
BackHandler,
|
||||
Linking,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
View,
|
||||
} from 'react-native';
|
||||
@@ -26,6 +28,14 @@ import { WebViewFooter } from '@/components/WebViewFooter';
|
||||
import { selfUrl } from '@/consts/links';
|
||||
import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout';
|
||||
import type { SharedRoutesParamList } from '@/navigation/types';
|
||||
import {
|
||||
DISALLOWED_SCHEMES,
|
||||
isAllowedAboutUrl,
|
||||
isHostnameMatch,
|
||||
isTrustedDomain,
|
||||
isUserInitiatedTopFrameNavigation,
|
||||
shouldAlwaysOpenExternally,
|
||||
} from '@/utils/webview';
|
||||
|
||||
export interface WebViewScreenParams {
|
||||
url: string;
|
||||
@@ -41,6 +51,25 @@ type WebViewScreenProps = NativeStackScreenProps<
|
||||
>;
|
||||
|
||||
const defaultUrl = selfUrl;
|
||||
const fallbackUrl = 'https://apps.self.xyz';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
webViewContainer: {
|
||||
flex: 1,
|
||||
alignSelf: 'stretch',
|
||||
backgroundColor: white,
|
||||
},
|
||||
webView: {
|
||||
flex: 1,
|
||||
backgroundColor: white,
|
||||
},
|
||||
loadingOverlay: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: 'rgba(255,255,255,0.5)',
|
||||
},
|
||||
});
|
||||
|
||||
export const WebViewScreen: React.FC<WebViewScreenProps> = ({ route }) => {
|
||||
const navigation = useNavigation();
|
||||
@@ -50,24 +79,83 @@ export const WebViewScreen: React.FC<WebViewScreenProps> = ({ route }) => {
|
||||
const isHttpUrl = useCallback((value?: string) => {
|
||||
return typeof value === 'string' && /^https?:\/\//i.test(value);
|
||||
}, []);
|
||||
const initialUrl = useMemo(
|
||||
() => (isHttpUrl(url) ? url : defaultUrl),
|
||||
[isHttpUrl, url],
|
||||
);
|
||||
const initialUrl = useMemo(() => {
|
||||
if (isHttpUrl(url) && isTrustedDomain(url)) {
|
||||
return url;
|
||||
}
|
||||
if (isHttpUrl(defaultUrl) && isTrustedDomain(defaultUrl)) {
|
||||
return defaultUrl;
|
||||
}
|
||||
return fallbackUrl;
|
||||
}, [isHttpUrl, url]);
|
||||
const webViewRef = useRef<WebViewType>(null);
|
||||
const [canGoBackInWebView, setCanGoBackInWebView] = useState(false);
|
||||
const [canGoForwardInWebView, setCanGoForwardInWebView] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [currentUrl, setCurrentUrl] = useState(initialUrl);
|
||||
const [pageTitle, setPageTitle] = useState<string | undefined>(title);
|
||||
const [isSessionTrusted, setIsSessionTrusted] = useState(
|
||||
isTrustedDomain(initialUrl),
|
||||
);
|
||||
|
||||
const derivedTitle = pageTitle || title || currentUrl;
|
||||
|
||||
/**
|
||||
* Show a confirmation dialog before opening a URL externally.
|
||||
* Returns true if user confirms, false if they cancel.
|
||||
*/
|
||||
const confirmExternalNavigation = useCallback(
|
||||
(context: 'wallet' | 'deep-link' | 'external-site'): Promise<boolean> => {
|
||||
return new Promise(resolve => {
|
||||
const messages: Record<
|
||||
typeof context,
|
||||
{ title: string; body: string }
|
||||
> = {
|
||||
wallet: {
|
||||
title: 'Open in Browser',
|
||||
body: 'This will open in your browser to complete the wallet connection.',
|
||||
},
|
||||
'deep-link': {
|
||||
title: 'Open External App',
|
||||
body: 'This will open an external app.',
|
||||
},
|
||||
'external-site': {
|
||||
title: 'Open in Browser',
|
||||
body: 'This will open an external website in your browser.',
|
||||
},
|
||||
};
|
||||
|
||||
const { title: alertTitle, body } = messages[context];
|
||||
|
||||
Alert.alert(alertTitle, body, [
|
||||
{ text: 'Cancel', style: 'cancel', onPress: () => resolve(false) },
|
||||
{ text: 'Open', onPress: () => resolve(true) },
|
||||
]);
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const openUrl = useCallback(async (targetUrl: string) => {
|
||||
// Allow only safe external schemes
|
||||
if (!/^(https?|mailto|tel):/i.test(targetUrl)) {
|
||||
// Block disallowed schemes (blacklist approach)
|
||||
// Allow everything else - more practical than maintaining a whitelist
|
||||
const isDisallowed = DISALLOWED_SCHEMES.some(scheme =>
|
||||
targetUrl.toLowerCase().startsWith(scheme.toLowerCase()),
|
||||
);
|
||||
if (isDisallowed) {
|
||||
// Block disallowed schemes - don't attempt to open
|
||||
return;
|
||||
}
|
||||
// Block about:blank and similar about: URLs - they're not meant to be opened externally
|
||||
if (targetUrl.toLowerCase().startsWith('about:')) {
|
||||
// Silently ignore about: URLs - they're internal browser navigation
|
||||
return;
|
||||
}
|
||||
// Validate URL has a valid scheme pattern
|
||||
if (!/^[a-zA-Z][a-zA-Z0-9+.-]*:/i.test(targetUrl)) {
|
||||
return;
|
||||
}
|
||||
// Attempt to open the URL
|
||||
try {
|
||||
const supported = await Linking.canOpenURL(targetUrl);
|
||||
if (supported) {
|
||||
@@ -115,16 +203,23 @@ export const WebViewScreen: React.FC<WebViewScreenProps> = ({ route }) => {
|
||||
const subscription = BackHandler.addEventListener(
|
||||
'hardwareBackPress',
|
||||
() => {
|
||||
// First try to go back in WebView if possible
|
||||
if (canGoBackInWebView) {
|
||||
webViewRef.current?.goBack();
|
||||
return true;
|
||||
}
|
||||
// If WebView can't go back, close the WebView screen (go back in navigation)
|
||||
if (navigation?.canGoBack()) {
|
||||
navigation.goBack();
|
||||
return true;
|
||||
}
|
||||
// Only allow default behavior (close app) if navigation can't go back
|
||||
return false;
|
||||
},
|
||||
);
|
||||
|
||||
return () => subscription.remove();
|
||||
}, [canGoBackInWebView]),
|
||||
}, [canGoBackInWebView, navigation]),
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -134,6 +229,7 @@ export const WebViewScreen: React.FC<WebViewScreenProps> = ({ route }) => {
|
||||
alignItems="stretch"
|
||||
justifyContent="flex-start"
|
||||
padding={0}
|
||||
paddingHorizontal={5}
|
||||
>
|
||||
<WebViewNavBar
|
||||
title={derivedTitle}
|
||||
@@ -149,18 +245,158 @@ export const WebViewScreen: React.FC<WebViewScreenProps> = ({ route }) => {
|
||||
<WebView
|
||||
ref={webViewRef}
|
||||
onShouldStartLoadWithRequest={req => {
|
||||
// Open non-http(s) externally, block in WebView
|
||||
if (!/^https?:\/\//i.test(req.url)) {
|
||||
openUrl(req.url);
|
||||
const isHttps = /^https:\/\//i.test(req.url);
|
||||
|
||||
// Allow about:blank/srcdoc during trusted sessions (some wallets use this before redirecting)
|
||||
if (isSessionTrusted && isAllowedAboutUrl(req.url)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// iOS-specific: Detect WalletConnect attestation from Aave and kick to Safari
|
||||
// WalletConnect doesn't work properly in WKWebView for Coinbase Wallet connections
|
||||
// Use hostname matching to prevent spoofing (e.g., evil.com/?next=verify.walletconnect.org)
|
||||
if (
|
||||
Platform.OS === 'ios' &&
|
||||
isHostnameMatch(req.url, 'verify.walletconnect.org') &&
|
||||
req.mainDocumentURL &&
|
||||
isHostnameMatch(req.mainDocumentURL, 'app.aave.com')
|
||||
) {
|
||||
// Kick parent page to Safari for wallet connection
|
||||
confirmExternalNavigation('wallet').then(confirmed => {
|
||||
if (confirmed) {
|
||||
openUrl(currentUrl);
|
||||
}
|
||||
});
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
// Open non-http(s) schemes externally (mailto, tel, etc.)
|
||||
// iOS: only allow top-frame, user-initiated navigations to prevent
|
||||
// drive-by deep-linking via iframes on trusted partner sites
|
||||
if (!/^https?:\/\//i.test(req.url)) {
|
||||
if (isUserInitiatedTopFrameNavigation(req)) {
|
||||
// Show confirmation before opening deep-link schemes
|
||||
confirmExternalNavigation('deep-link').then(confirmed => {
|
||||
if (confirmed) {
|
||||
openUrl(req.url);
|
||||
}
|
||||
});
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Enforce "always open externally" policy before any other checks
|
||||
// (e.g., keys.coinbase.com requires window.opener in full browser)
|
||||
if (shouldAlwaysOpenExternally(req.url)) {
|
||||
// Show confirmation before redirecting to external wallet
|
||||
confirmExternalNavigation('wallet').then(confirmed => {
|
||||
if (confirmed) {
|
||||
// Open the current page externally to maintain window.opener
|
||||
openUrl(currentUrl);
|
||||
}
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
const trusted = isTrustedDomain(req.url);
|
||||
|
||||
// Allow trusted entrypoints and mark session trusted
|
||||
if (trusted) {
|
||||
if (!isSessionTrusted) {
|
||||
setIsSessionTrusted(true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Parent-trusted session model: allow HTTPS child navigations
|
||||
// after a trusted entrypoint to avoid breaking on partner deps.
|
||||
if (isSessionTrusted && isHttps) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Untrusted navigation without a trusted session: open externally
|
||||
// iOS: only allow top-frame, user-initiated navigations
|
||||
if (isUserInitiatedTopFrameNavigation(req)) {
|
||||
// Show confirmation before opening untrusted external site
|
||||
confirmExternalNavigation('external-site').then(confirmed => {
|
||||
if (confirmed) {
|
||||
openUrl(req.url);
|
||||
}
|
||||
});
|
||||
}
|
||||
return false;
|
||||
}}
|
||||
onOpenWindow={syntheticEvent => {
|
||||
// Handle links that try to open in new window (target="_blank")
|
||||
const { nativeEvent } = syntheticEvent;
|
||||
const targetUrl = nativeEvent.targetUrl;
|
||||
|
||||
if (targetUrl) {
|
||||
// Coinbase wallet uses window.opener.postMessage from the popup back to
|
||||
// the parent page. If we only open the popup externally and keep the
|
||||
// parent inside the WebView, the popup cannot find window.opener and the
|
||||
// SDK times out. Redirect the parent page (currentUrl) to a real browser
|
||||
// context; if we somehow don't know the parent URL, fall back to opening
|
||||
// the popup target directly.
|
||||
if (shouldAlwaysOpenExternally(targetUrl)) {
|
||||
// Show confirmation before redirecting to external wallet
|
||||
confirmExternalNavigation('wallet').then(confirmed => {
|
||||
if (confirmed) {
|
||||
openUrl(currentUrl || targetUrl);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Some sites open about:blank/srcdoc before redirecting; allow silently
|
||||
if (isSessionTrusted && isAllowedAboutUrl(targetUrl)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Allow trusted domains to load in the current WebView
|
||||
const trusted = isTrustedDomain(targetUrl);
|
||||
if (trusted) {
|
||||
if (!isSessionTrusted) {
|
||||
setIsSessionTrusted(true);
|
||||
}
|
||||
webViewRef.current?.injectJavaScript(
|
||||
`window.location.href = ${JSON.stringify(targetUrl)};`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Parent-trusted session model: allow HTTPS child navigations via window.open
|
||||
// after a trusted entrypoint to avoid breaking on partner deps.
|
||||
if (isSessionTrusted && /^https:\/\//i.test(targetUrl)) {
|
||||
webViewRef.current?.injectJavaScript(
|
||||
`window.location.href = ${JSON.stringify(targetUrl)};`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Security: Block non-HTTPS/non-trusted window.open calls to prevent
|
||||
// drive-by deep-linking from iframes on trusted sites. Unlike
|
||||
// onShouldStartLoadWithRequest, onOpenWindow doesn't expose frame-origin
|
||||
// metadata, so we cannot verify if this is user-initiated top-frame
|
||||
// navigation. Block silently to maintain security without breaking UX.
|
||||
}
|
||||
}}
|
||||
// Enable multiple windows to let WKWebView forward window.open;
|
||||
// we still force navigation into the same WebView via onOpenWindow.
|
||||
setSupportMultipleWindows
|
||||
source={{ uri: initialUrl }}
|
||||
onNavigationStateChange={(event: WebViewNavigation) => {
|
||||
setCanGoBackInWebView(event.canGoBack);
|
||||
setCanGoForwardInWebView(event.canGoForward);
|
||||
setCurrentUrl(prev => (isHttpUrl(event.url) ? event.url : prev));
|
||||
// Only mark session as trusted if the domain is trusted AND not in always-external list
|
||||
// (e.g., keys.coinbase.com should never establish a trusted session)
|
||||
if (
|
||||
isTrustedDomain(event.url) &&
|
||||
!shouldAlwaysOpenExternally(event.url)
|
||||
) {
|
||||
setIsSessionTrusted(true);
|
||||
}
|
||||
if (!title && event.title) {
|
||||
setPageTitle(event.title);
|
||||
}
|
||||
@@ -174,8 +410,8 @@ export const WebViewScreen: React.FC<WebViewScreenProps> = ({ route }) => {
|
||||
</ExpandableBottomLayout.TopSection>
|
||||
<ExpandableBottomLayout.BottomSection
|
||||
backgroundColor={white}
|
||||
borderTopLeftRadius={30}
|
||||
borderTopRightRadius={30}
|
||||
borderTopLeftRadius={20}
|
||||
borderTopRightRadius={20}
|
||||
borderTopWidth={1}
|
||||
borderColor={slate200}
|
||||
style={{ paddingTop: 0 }}
|
||||
@@ -192,21 +428,3 @@ export const WebViewScreen: React.FC<WebViewScreenProps> = ({ route }) => {
|
||||
</ExpandableBottomLayout.Layout>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
webViewContainer: {
|
||||
flex: 1,
|
||||
alignSelf: 'stretch',
|
||||
backgroundColor: white,
|
||||
},
|
||||
webView: {
|
||||
flex: 1,
|
||||
backgroundColor: white,
|
||||
},
|
||||
loadingOverlay: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: 'rgba(255,255,255,0.5)',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -8,3 +8,4 @@
|
||||
* Use this constant instead of checking __DEV__ directly throughout the codebase.
|
||||
*/
|
||||
export const IS_DEV_MODE = typeof __DEV__ !== 'undefined' && __DEV__;
|
||||
export const IS_EUCLID_ENABLED = false; //IS_DEV_MODE; // just in case we forgot to turn it off before pushing to prod.
|
||||
|
||||
@@ -9,6 +9,19 @@
|
||||
|
||||
// Crypto utilities
|
||||
export type { ModalCallbacks } from '@/utils/modalCallbackRegistry';
|
||||
|
||||
// WebView utilities
|
||||
export type { WebViewRequestWithIosProps } from '@/utils/webview';
|
||||
|
||||
export {
|
||||
DISALLOWED_SCHEMES,
|
||||
TRUSTED_DOMAINS,
|
||||
isAllowedAboutUrl,
|
||||
isSameOrigin,
|
||||
isTrustedDomain,
|
||||
isUserInitiatedTopFrameNavigation,
|
||||
} from '@/utils/webview';
|
||||
|
||||
// Format utilities
|
||||
export { IS_DEV_MODE } from '@/utils/devUtils';
|
||||
|
||||
|
||||
193
app/src/utils/webview.ts
Normal file
193
app/src/utils/webview.ts
Normal file
@@ -0,0 +1,193 @@
|
||||
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import { Platform } from 'react-native';
|
||||
|
||||
/**
|
||||
* WebView request object with iOS-specific properties for navigation control.
|
||||
* Used to determine if a navigation is user-initiated and from the top frame.
|
||||
*/
|
||||
export interface WebViewRequestWithIosProps {
|
||||
isTopFrame?: boolean;
|
||||
navigationType?:
|
||||
| 'click'
|
||||
| 'formsubmit'
|
||||
| 'formresubmit'
|
||||
| 'backforward'
|
||||
| 'reload'
|
||||
| 'other';
|
||||
}
|
||||
|
||||
/**
|
||||
* Domains that should always open externally (e.g., wallet popups that require
|
||||
* a full browser context to maintain window.opener relationship).
|
||||
*/
|
||||
export const ALWAYS_OPEN_EXTERNALLY = Object.freeze([
|
||||
'keys.coinbase.com',
|
||||
]) as readonly string[];
|
||||
|
||||
/**
|
||||
* Schemes that are disallowed from being opened externally.
|
||||
* Using a blacklist approach - block specific dangerous schemes, allow everything else.
|
||||
* Includes both variants (with and without '://') to catch all forms of these schemes.
|
||||
*/
|
||||
export const DISALLOWED_SCHEMES = Object.freeze([
|
||||
'ftp://',
|
||||
'ftp:',
|
||||
'ftps://',
|
||||
'ftps:',
|
||||
'file://',
|
||||
'file:',
|
||||
// eslint-disable-next-line no-script-url
|
||||
'javascript:',
|
||||
'data:',
|
||||
'blob:',
|
||||
]) as readonly string[];
|
||||
|
||||
/**
|
||||
* Trusted entrypoints: these domains are allowed to start a session.
|
||||
* Once a session starts from a trusted domain, HTTPS child navigations are
|
||||
* allowed without expanding this list (parent-trusted session model).
|
||||
* This keeps partners from breaking the WebView when they add dependencies,
|
||||
* while still requiring the initial navigation to be curated.
|
||||
*
|
||||
* Note: Domains in ALWAYS_OPEN_EXTERNALLY (e.g., keys.coinbase.com) are
|
||||
* excluded from this list as they require full browser context and cannot
|
||||
* be trusted WebView entrypoints.
|
||||
*/
|
||||
export const TRUSTED_DOMAINS = Object.freeze([
|
||||
'aave.com', // Aave protocol - DeFi lending network
|
||||
'amity-lock-11401309.figma.site', // Degen Tarot game
|
||||
'celo.org', // CELO Names - includes names.celo.org
|
||||
'cloud.google.com', // Google Cloud - AI agents in the cloud (includes cloud.google.com)
|
||||
'coinbase.com', // Coinbase - Main domain
|
||||
'karmahq.xyz', // Karma - Launch & fund projects
|
||||
'lemonade.social', // Lemonade - Events and communities
|
||||
'self.xyz', // Base domain and all subdomains (*.self.xyz) - includes espresso.self.xyz
|
||||
'talent.app', // Talent Protocol - Main app
|
||||
'talentprotocol.com', // Talent Protocol - Marketing/info site
|
||||
'velodrome.finance', // Velodrome - Swap, deposit, take the lead
|
||||
]) as readonly string[];
|
||||
|
||||
/**
|
||||
* Check if a URL is an allowed about: URL (about:blank or about:srcdoc).
|
||||
* These URLs are allowed during trusted sessions for wallet bootstrap flows.
|
||||
*/
|
||||
export const isAllowedAboutUrl = (url: string): boolean => {
|
||||
const lower = url.toLowerCase();
|
||||
return lower === 'about:blank' || lower === 'about:srcdoc';
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if a URL's hostname matches a given domain (exact or subdomain match).
|
||||
* Returns false for malformed URLs or if the URL doesn't match.
|
||||
*
|
||||
* @param url - The URL to check
|
||||
* @param domain - The domain to match against (e.g., 'example.com')
|
||||
* @returns true if hostname matches domain or is a subdomain of it
|
||||
*
|
||||
* @example
|
||||
* isHostnameMatch('https://example.com/path', 'example.com') // true
|
||||
* isHostnameMatch('https://sub.example.com/path', 'example.com') // true
|
||||
* isHostnameMatch('https://evil.com/?next=example.com', 'example.com') // false
|
||||
*/
|
||||
export const isHostnameMatch = (url: string, domain: string): boolean => {
|
||||
try {
|
||||
const hostname = new URL(url).hostname;
|
||||
return hostname === domain || hostname.endsWith(`.${domain}`);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if two URLs have the same origin (protocol + host + port).
|
||||
* Returns false for malformed URLs.
|
||||
*/
|
||||
export const isSameOrigin = (url1: string, url2: string): boolean => {
|
||||
try {
|
||||
return new URL(url1).origin === new URL(url2).origin;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if a URL is from a trusted domain.
|
||||
* Matches exact domain or any subdomain of trusted domains.
|
||||
* Returns false for malformed URLs.
|
||||
*
|
||||
* Note: Domains in ALWAYS_OPEN_EXTERNALLY (e.g., keys.coinbase.com) are
|
||||
* excluded even if they would match as subdomains of trusted domains.
|
||||
*/
|
||||
export const isTrustedDomain = (url: string): boolean => {
|
||||
try {
|
||||
const hostname = new URL(url).hostname;
|
||||
|
||||
// First check if this domain should always open externally
|
||||
// These domains cannot be trusted entrypoints even if they're subdomains of trusted domains
|
||||
const alwaysExternal = ALWAYS_OPEN_EXTERNALLY.some(
|
||||
domain => hostname === domain || hostname.endsWith(`.${domain}`),
|
||||
);
|
||||
if (alwaysExternal) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Then check if it matches any trusted domain
|
||||
return TRUSTED_DOMAINS.some(
|
||||
domain => hostname === domain || hostname.endsWith(`.${domain}`),
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* iOS-only mitigation for drive-by deep-linking via iframes.
|
||||
* Gates external URL opens to top-frame, user-initiated navigations.
|
||||
*
|
||||
* On iOS, isTopFrame and navigationType are available on the request object.
|
||||
* On Android, these properties are unavailable, so we allow all navigations.
|
||||
*
|
||||
* This prevents malicious iframes on trusted partner sites from invoking
|
||||
* external app opens (sms:, mailto:, etc.) without explicit user interaction.
|
||||
*/
|
||||
export const isUserInitiatedTopFrameNavigation = (
|
||||
req: WebViewRequestWithIosProps,
|
||||
): boolean => {
|
||||
// Android: these properties are unavailable, allow all navigations
|
||||
if (Platform.OS !== 'ios') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// iOS: block if explicitly from an iframe
|
||||
if (req.isTopFrame === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// iOS: only allow 'click' or undefined (backward compatibility) navigations
|
||||
// Block 'other', 'reload', 'formsubmit', 'backforward' as non-user-initiated
|
||||
const navType = req.navigationType;
|
||||
if (navType !== undefined && navType !== 'click') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Determine if a URL should always be opened externally.
|
||||
* Used for special cases like Coinbase wallet that require window.opener.
|
||||
* Returns false for malformed URLs.
|
||||
*/
|
||||
export const shouldAlwaysOpenExternally = (url: string): boolean => {
|
||||
try {
|
||||
const hostname = new URL(url).hostname;
|
||||
return ALWAYS_OPEN_EXTERNALLY.some(
|
||||
domain => hostname === domain || hostname.endsWith(`.${domain}`),
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -44,7 +44,7 @@ describe('parseScanResponse', () => {
|
||||
global.mockPlatformOS = 'ios';
|
||||
});
|
||||
|
||||
it('parses iOS response', () => {
|
||||
it.skip('parses iOS response', () => {
|
||||
// Platform.OS is already mocked as 'ios' by default
|
||||
const mrz =
|
||||
'P<UTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<L898902C<3UTO6908061F9406236ZE184226B<<<<<14';
|
||||
@@ -61,8 +61,45 @@ describe('parseScanResponse', () => {
|
||||
passportPhoto: 'photo',
|
||||
documentSigningCertificate: JSON.stringify({ PEM: 'CERT' }),
|
||||
});
|
||||
|
||||
expect(response).toMatchInlineSnapshot(
|
||||
`"{"dataGroupHashes":"{\\"DG1\\":{\\"sodHash\\":\\"abcd\\"},\\"DG2\\":{\\"sodHash\\":\\"1234\\"}}","eContentBase64":"ZWM=","signedAttributes":"c2E=","passportMRZ":"P<UTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<L898902C<3UTO6908061F9406236ZE184226B<<<<<14","signatureBase64":"AQI=","dataGroupsPresent":[1,2],"passportPhoto":"photo","documentSigningCertificate":"{\\"PEM\\":\\"CERT\\"}"}"`,
|
||||
);
|
||||
const result = parseScanResponse(response);
|
||||
console.log('Parsed Result:', result);
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
{
|
||||
"dg1Hash": [
|
||||
171,
|
||||
205,
|
||||
],
|
||||
"dg2Hash": [
|
||||
18,
|
||||
52,
|
||||
],
|
||||
"dgPresents": [
|
||||
1,
|
||||
2,
|
||||
],
|
||||
"documentCategory": "passport",
|
||||
"documentType": "passport",
|
||||
"dsc": "CERT",
|
||||
"eContent": [
|
||||
101,
|
||||
99,
|
||||
],
|
||||
"encryptedDigest": [
|
||||
1,
|
||||
2,
|
||||
],
|
||||
"mock": false,
|
||||
"mrz": "P<UTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<L898902C<3UTO6908061F9406236ZE184226B<<<<<14",
|
||||
"parsed": false,
|
||||
"signedAttr": [
|
||||
115,
|
||||
97,
|
||||
],
|
||||
}
|
||||
`);
|
||||
expect(result.mrz).toBe(mrz);
|
||||
expect(result.documentType).toBe('passport');
|
||||
// 'abcd' in hex: ab = 171, cd = 205
|
||||
@@ -86,8 +123,50 @@ describe('parseScanResponse', () => {
|
||||
// Android format: '1' and '2' are hex strings, not arrays
|
||||
dataGroupHashes: JSON.stringify({ '1': 'abcd', '2': '1234' }),
|
||||
} as any;
|
||||
|
||||
expect(response).toMatchInlineSnapshot(`
|
||||
{
|
||||
"dataGroupHashes": "{"1":"abcd","2":"1234"}",
|
||||
"documentSigningCertificate": "CERT",
|
||||
"eContent": "[4,5]",
|
||||
"encapContent": "[8,9]",
|
||||
"encryptedDigest": "[6,7]",
|
||||
"mrz": "P<UTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<L898902C<3UTO6908061F9406236ZE184226B<<<<<14",
|
||||
}
|
||||
`);
|
||||
const result = parseScanResponse(response);
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
{
|
||||
"dg1Hash": [
|
||||
171,
|
||||
205,
|
||||
],
|
||||
"dg2Hash": [
|
||||
18,
|
||||
52,
|
||||
],
|
||||
"dgPresents": [
|
||||
1,
|
||||
2,
|
||||
],
|
||||
"documentCategory": "passport",
|
||||
"documentType": "passport",
|
||||
"dsc": "-----BEGIN CERTIFICATE-----CERT-----END CERTIFICATE-----",
|
||||
"eContent": [
|
||||
8,
|
||||
9,
|
||||
],
|
||||
"encryptedDigest": [
|
||||
6,
|
||||
7,
|
||||
],
|
||||
"mock": false,
|
||||
"mrz": "P<UTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<L898902C<3UTO6908061F9406236ZE184226B<<<<<14",
|
||||
"signedAttr": [
|
||||
4,
|
||||
5,
|
||||
],
|
||||
}
|
||||
`);
|
||||
expect(result.documentType).toBe('passport');
|
||||
expect(result.mrz).toBe(mrz);
|
||||
// 'abcd' in hex: ab = 171, cd = 205
|
||||
|
||||
@@ -19,7 +19,9 @@ jest.mock('@/navigation', () => {
|
||||
// Documents screens
|
||||
IDPicker: {},
|
||||
IdDetails: {},
|
||||
CountryPicker: {},
|
||||
CountryPicker: {
|
||||
statusBar: { hidden: true, style: 'dark' },
|
||||
},
|
||||
DocumentCamera: {},
|
||||
DocumentCameraTrouble: {},
|
||||
DocumentDataInfo: {},
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
258
app/tests/src/utils/webview.test.ts
Normal file
258
app/tests/src/utils/webview.test.ts
Normal file
@@ -0,0 +1,258 @@
|
||||
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
|
||||
|
||||
import {
|
||||
ALWAYS_OPEN_EXTERNALLY,
|
||||
isAllowedAboutUrl,
|
||||
isHostnameMatch,
|
||||
isSameOrigin,
|
||||
isTrustedDomain,
|
||||
shouldAlwaysOpenExternally,
|
||||
TRUSTED_DOMAINS,
|
||||
} from '@/utils/webview';
|
||||
|
||||
describe('webview utilities', () => {
|
||||
describe('isHostnameMatch', () => {
|
||||
it('should match exact domain', () => {
|
||||
expect(isHostnameMatch('https://example.com', 'example.com')).toBe(true);
|
||||
expect(isHostnameMatch('https://example.com/', 'example.com')).toBe(true);
|
||||
expect(isHostnameMatch('https://example.com/path', 'example.com')).toBe(
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
isHostnameMatch('https://example.com/path?query=1', 'example.com'),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should match subdomains', () => {
|
||||
expect(isHostnameMatch('https://sub.example.com', 'example.com')).toBe(
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
isHostnameMatch('https://sub.sub.example.com', 'example.com'),
|
||||
).toBe(true);
|
||||
expect(isHostnameMatch('https://www.example.com', 'example.com')).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('should NOT match domain in query parameters (spoofing attempt)', () => {
|
||||
expect(
|
||||
isHostnameMatch('https://evil.com/?next=example.com', 'example.com'),
|
||||
).toBe(false);
|
||||
expect(
|
||||
isHostnameMatch(
|
||||
'https://evil.com/path?redirect=example.com',
|
||||
'example.com',
|
||||
),
|
||||
).toBe(false);
|
||||
expect(
|
||||
isHostnameMatch('https://attacker.com#example.com', 'example.com'),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should NOT match domain in path (spoofing attempt)', () => {
|
||||
expect(
|
||||
isHostnameMatch('https://evil.com/example.com', 'example.com'),
|
||||
).toBe(false);
|
||||
expect(
|
||||
isHostnameMatch('https://evil.com/path/example.com', 'example.com'),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should NOT match similar but different domains', () => {
|
||||
expect(isHostnameMatch('https://example.org', 'example.com')).toBe(false);
|
||||
expect(isHostnameMatch('https://notexample.com', 'example.com')).toBe(
|
||||
false,
|
||||
);
|
||||
expect(
|
||||
isHostnameMatch('https://example.com.evil.com', 'example.com'),
|
||||
).toBe(false);
|
||||
expect(isHostnameMatch('https://fakeexample.com', 'example.com')).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle malformed URLs gracefully', () => {
|
||||
expect(isHostnameMatch('not a url', 'example.com')).toBe(false);
|
||||
expect(isHostnameMatch('', 'example.com')).toBe(false);
|
||||
// eslint-disable-next-line no-script-url
|
||||
expect(isHostnameMatch('javascript:alert(1)', 'example.com')).toBe(false);
|
||||
expect(isHostnameMatch('ftp://example.com', 'example.com')).toBe(true); // valid URL, different protocol
|
||||
});
|
||||
|
||||
it('should be case-insensitive for hostnames', () => {
|
||||
expect(isHostnameMatch('https://Example.COM', 'example.com')).toBe(true);
|
||||
expect(isHostnameMatch('https://EXAMPLE.COM', 'example.com')).toBe(true);
|
||||
});
|
||||
|
||||
describe('WalletConnect spoofing protection', () => {
|
||||
it('should match legitimate WalletConnect URLs', () => {
|
||||
expect(
|
||||
isHostnameMatch(
|
||||
'https://verify.walletconnect.org/v3/attestation',
|
||||
'verify.walletconnect.org',
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
isHostnameMatch(
|
||||
'https://verify.walletconnect.org/path?query=1',
|
||||
'verify.walletconnect.org',
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should NOT match spoofed WalletConnect URLs', () => {
|
||||
expect(
|
||||
isHostnameMatch(
|
||||
'https://evil.com/?next=verify.walletconnect.org',
|
||||
'verify.walletconnect.org',
|
||||
),
|
||||
).toBe(false);
|
||||
expect(
|
||||
isHostnameMatch(
|
||||
'https://evil.com/verify.walletconnect.org',
|
||||
'verify.walletconnect.org',
|
||||
),
|
||||
).toBe(false);
|
||||
expect(
|
||||
isHostnameMatch(
|
||||
'https://verify.walletconnect.org.evil.com',
|
||||
'verify.walletconnect.org',
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Aave spoofing protection', () => {
|
||||
it('should match legitimate Aave URLs', () => {
|
||||
expect(isHostnameMatch('https://app.aave.com', 'app.aave.com')).toBe(
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
isHostnameMatch('https://app.aave.com/markets', 'app.aave.com'),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should NOT match spoofed Aave URLs', () => {
|
||||
expect(
|
||||
isHostnameMatch(
|
||||
'https://evil.com/?redirect=app.aave.com',
|
||||
'app.aave.com',
|
||||
),
|
||||
).toBe(false);
|
||||
expect(
|
||||
isHostnameMatch('https://evil.com/app.aave.com', 'app.aave.com'),
|
||||
).toBe(false);
|
||||
expect(
|
||||
isHostnameMatch('https://app.aave.com.evil.com', 'app.aave.com'),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('isTrustedDomain', () => {
|
||||
it('should match domains from TRUSTED_DOMAINS list', () => {
|
||||
TRUSTED_DOMAINS.forEach(domain => {
|
||||
expect(isTrustedDomain(`https://${domain}`)).toBe(true);
|
||||
expect(isTrustedDomain(`https://www.${domain}`)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not match untrusted domains', () => {
|
||||
expect(isTrustedDomain('https://evil.com')).toBe(false);
|
||||
expect(isTrustedDomain('https://attacker.org')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('shouldAlwaysOpenExternally', () => {
|
||||
it('should match domains from ALWAYS_OPEN_EXTERNALLY list', () => {
|
||||
ALWAYS_OPEN_EXTERNALLY.forEach(domain => {
|
||||
expect(shouldAlwaysOpenExternally(`https://${domain}`)).toBe(true);
|
||||
expect(shouldAlwaysOpenExternally(`https://www.${domain}`)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not match other domains', () => {
|
||||
expect(shouldAlwaysOpenExternally('https://example.com')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Policy: keys.coinbase.com always opens externally', () => {
|
||||
it('should be in ALWAYS_OPEN_EXTERNALLY list', () => {
|
||||
expect(ALWAYS_OPEN_EXTERNALLY).toContain('keys.coinbase.com');
|
||||
});
|
||||
|
||||
it('should NOT be in TRUSTED_DOMAINS list (policy conflict prevention)', () => {
|
||||
expect(TRUSTED_DOMAINS).not.toContain('keys.coinbase.com');
|
||||
});
|
||||
|
||||
it('should return true for shouldAlwaysOpenExternally', () => {
|
||||
expect(shouldAlwaysOpenExternally('https://keys.coinbase.com')).toBe(
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
shouldAlwaysOpenExternally('https://keys.coinbase.com/connect'),
|
||||
).toBe(true);
|
||||
expect(
|
||||
shouldAlwaysOpenExternally('https://keys.coinbase.com/path?query=1'),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for isTrustedDomain', () => {
|
||||
expect(isTrustedDomain('https://keys.coinbase.com')).toBe(false);
|
||||
expect(isTrustedDomain('https://keys.coinbase.com/connect')).toBe(false);
|
||||
});
|
||||
|
||||
it('should correctly identify that keys.coinbase.com cannot be a trusted entrypoint', () => {
|
||||
// Verify the policy: if a domain should always open externally,
|
||||
// it cannot be trusted in the WebView
|
||||
const url = 'https://keys.coinbase.com/wallet';
|
||||
expect(shouldAlwaysOpenExternally(url)).toBe(true);
|
||||
expect(isTrustedDomain(url)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isAllowedAboutUrl', () => {
|
||||
it('should allow about:blank and about:srcdoc', () => {
|
||||
expect(isAllowedAboutUrl('about:blank')).toBe(true);
|
||||
expect(isAllowedAboutUrl('about:srcdoc')).toBe(true);
|
||||
expect(isAllowedAboutUrl('ABOUT:BLANK')).toBe(true);
|
||||
expect(isAllowedAboutUrl('ABOUT:SRCDOC')).toBe(true);
|
||||
});
|
||||
|
||||
it('should not allow other about: URLs', () => {
|
||||
expect(isAllowedAboutUrl('about:config')).toBe(false);
|
||||
expect(isAllowedAboutUrl('about:plugins')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isSameOrigin', () => {
|
||||
it('should return true for same origin URLs', () => {
|
||||
expect(
|
||||
isSameOrigin('https://example.com/path1', 'https://example.com/path2'),
|
||||
).toBe(true);
|
||||
expect(
|
||||
isSameOrigin('https://example.com:443/', 'https://example.com/'),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for different origins', () => {
|
||||
expect(isSameOrigin('https://example.com', 'https://other.com')).toBe(
|
||||
false,
|
||||
);
|
||||
expect(isSameOrigin('https://example.com', 'http://example.com')).toBe(
|
||||
false,
|
||||
);
|
||||
expect(
|
||||
isSameOrigin('https://example.com:443', 'https://example.com:8443'),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle malformed URLs gracefully', () => {
|
||||
expect(isSameOrigin('not a url', 'https://example.com')).toBe(false);
|
||||
expect(isSameOrigin('https://example.com', 'not a url')).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"ios": {
|
||||
"build": 192,
|
||||
"lastDeployed": "2025-12-05T00:06:05.459Z"
|
||||
"build": 193,
|
||||
"lastDeployed": "2025-12-06T09:48:56.530Z"
|
||||
},
|
||||
"android": {
|
||||
"build": 123,
|
||||
"lastDeployed": "2025-11-21T00:06:05.459Z"
|
||||
"build": 124,
|
||||
"lastDeployed": "2025-12-06T09:48:56.530Z"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,7 +113,16 @@ template REGISTER_AADHAAR(n, k, maxDataLength){
|
||||
signal output nullifier <== nullifierHasher.out;
|
||||
|
||||
|
||||
signal qrDataHash <== PackBytesAndPoseidon(maxDataLength)(qrDataPadded);
|
||||
component qrDataHasher = PackBytesAndPoseidon(maxDataLength);
|
||||
for (var i = 0; i < 9; i++){
|
||||
qrDataHasher.in[i] <== qrDataPadded[i];
|
||||
}
|
||||
for (var i = 9; i < 26; i++) {
|
||||
qrDataHasher.in[i] <== 0;
|
||||
}
|
||||
for (var i = 26; i < maxDataLength; i++){
|
||||
qrDataHasher.in[i] <== qrDataPadded[i];
|
||||
}
|
||||
|
||||
// Generate commitment
|
||||
component packedCommitment = PackBytesAndPoseidon(42 + 62);
|
||||
@@ -138,7 +147,7 @@ template REGISTER_AADHAAR(n, k, maxDataLength){
|
||||
component commitmentHasher = Poseidon(5);
|
||||
|
||||
commitmentHasher.inputs[0] <== secret;
|
||||
commitmentHasher.inputs[1] <== qrDataHash;
|
||||
commitmentHasher.inputs[1] <== qrDataHasher.out;
|
||||
commitmentHasher.inputs[2] <== nullifierHasher.out;
|
||||
commitmentHasher.inputs[3] <== packedCommitment.out;
|
||||
commitmentHasher.inputs[4] <== qrDataExtractor.photoHash;
|
||||
|
||||
@@ -161,7 +161,7 @@ export const OFAC_TREE_LEVELS = 64;
|
||||
// we make it global here because passing it to generateCircuitInputsRegister caused trouble
|
||||
export const PASSPORT_ATTESTATION_ID = '1';
|
||||
|
||||
export const PCR0_MANAGER_ADDRESS = '0xE36d4EE5Fd3916e703A46C21Bb3837dB7680C8B8';
|
||||
export const PCR0_MANAGER_ADDRESS = '0x9743fe2C1c3D2b068c56dE314e9B10DA9c904717';
|
||||
|
||||
export const REDIRECT_URL = 'https://redirect.self.xyz';
|
||||
|
||||
|
||||
@@ -208,8 +208,6 @@ export function prepareAadhaarDiscloseData(
|
||||
secret,
|
||||
qrDataHash: formatInput(BigInt(sharedData.qrHash)),
|
||||
gender: formatInput(genderAscii),
|
||||
// qrDataHash: BigInt(sharedData.qrHash).toString(),
|
||||
// gender: genderAscii.toString(),
|
||||
yob: stringToAsciiArray(sharedData.extractedFields.yob),
|
||||
mob: stringToAsciiArray(sharedData.extractedFields.mob),
|
||||
dob: stringToAsciiArray(sharedData.extractedFields.dob),
|
||||
@@ -551,7 +549,6 @@ export function processQRData(
|
||||
QRData = newTestData.testQRData;
|
||||
} else {
|
||||
QRData = testQRData.testQRData;
|
||||
// console.log('testQRData:', testQRData);
|
||||
}
|
||||
|
||||
return processQRDataSimple(QRData);
|
||||
@@ -576,6 +573,13 @@ export function processQRDataSimple(qrData: string) {
|
||||
// Extract actual fields from QR data instead of using hardcoded values
|
||||
const extractedFields = extractQRDataFields(qrDataBytes);
|
||||
|
||||
// Calculate qrHash exclude timestamp (positions 9-25, 17 bytes)
|
||||
// const qrDataWithoutTimestamp = [
|
||||
// ...Array.from(qrDataPadded.slice(0, 9)),
|
||||
// ...Array.from(qrDataPadded.slice(9, 26)).map((x) => 0),
|
||||
// ...Array.from(qrDataPadded.slice(26)),
|
||||
// ];
|
||||
// const qrHash = packBytesAndPoseidon(qrDataWithoutTimestamp);
|
||||
const qrHash = packBytesAndPoseidon(Array.from(qrDataPadded));
|
||||
const photo = extractPhoto(Array.from(qrDataPadded), photoEOI + 1);
|
||||
|
||||
|
||||
@@ -398,7 +398,7 @@ export async function getAadharRegistrationWindow() {
|
||||
}
|
||||
|
||||
export function returnNewDateString(timestamp?: string): string {
|
||||
const newDate = timestamp ? new Date(+timestamp) : new Date();
|
||||
const newDate = timestamp ? new Date(+timestamp * 1000) : new Date();
|
||||
|
||||
// Convert the UTC date to IST by adding 5 hours and 30 minutes
|
||||
const offsetHours = 5;
|
||||
|
||||
@@ -87,21 +87,19 @@ function compareCertificates(cert1: forge.pki.Certificate, cert2: forge.pki.Cert
|
||||
}
|
||||
|
||||
function verifyCertificateChain({ leaf, intermediate, root }: PKICertificates) {
|
||||
const caStore = forge.pki.createCaStore([intermediate, root]);
|
||||
const caStore = forge.pki.createCaStore([root]);
|
||||
|
||||
forge.pki.verifyCertificateChain(caStore, [leaf], (vfd, depth, chain) => {
|
||||
if (!vfd) {
|
||||
forge.pki.verifyCertificateChain(caStore, [leaf, intermediate, root], (vfd, depth) => {
|
||||
if (vfd !== true) {
|
||||
throw new Error(`Certificate verification failed at depth ${depth}`);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
[leaf, intermediate, root].forEach((cert) => {
|
||||
const now = new Date();
|
||||
if (now < cert.validity.notBefore || now > cert.validity.notAfter) {
|
||||
throw new Error('Certificate is not within validity period');
|
||||
}
|
||||
});
|
||||
const now = new Date();
|
||||
if (now < root.validity.notBefore || now > root.validity.notAfter) {
|
||||
throw new Error('Certificate is not within validity period');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,3 +8,6 @@ CELO_RPC_URL=https://celo.drpc.org
|
||||
CELO_SEPOLIA_RPC_URL=https://rpc.ankr.com/celo_sepolia
|
||||
|
||||
ETHERSCAN_API_KEY=
|
||||
|
||||
STANDARD_GOVERNANCE_ADDRESS=
|
||||
CRITICAL_GOVERNANCE_ADDRESS=
|
||||
|
||||
214
contracts/UPGRADE_GUIDE.md
Normal file
214
contracts/UPGRADE_GUIDE.md
Normal file
@@ -0,0 +1,214 @@
|
||||
# Contract Upgrade Guide
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Update Your Contract
|
||||
|
||||
```solidity
|
||||
// Update version in NatSpec
|
||||
* @custom:version 2.13.0
|
||||
|
||||
// Update reinitializer modifier (increment by 1)
|
||||
function initialize(...) external reinitializer(13) {
|
||||
// Add any new initialization logic
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Run the Upgrade Script
|
||||
|
||||
```bash
|
||||
cd contracts
|
||||
npx hardhat upgrade --contract IdentityVerificationHub --network celo --changelog "Added feature X"
|
||||
```
|
||||
|
||||
### 3. Approve in Safe
|
||||
|
||||
The script outputs instructions to submit to the Safe multisig. Once 3/5 signers approve, execute the transaction.
|
||||
|
||||
---
|
||||
|
||||
## Governance Roles
|
||||
|
||||
| Role | Threshold | Purpose |
|
||||
| ----------------- | --------- | ------------------------------------ |
|
||||
| `SECURITY_ROLE` | 3/5 | Contract upgrades, role management |
|
||||
| `OPERATIONS_ROLE` | 2/5 | CSCA root updates, OFAC list updates |
|
||||
|
||||
---
|
||||
|
||||
## Detailed Workflow
|
||||
|
||||
### Step 1: Modify the Contract
|
||||
|
||||
1. Make your code changes
|
||||
2. Update `@custom:version` in the contract's NatSpec comment
|
||||
3. Increment the `reinitializer(N)` modifier (e.g., `reinitializer(12)` → `reinitializer(13)`)
|
||||
4. Add any new storage fields **at the end** of the storage struct
|
||||
|
||||
**Example:**
|
||||
|
||||
```solidity
|
||||
/**
|
||||
* @title IdentityVerificationHubImplV2
|
||||
* @custom:version 2.13.0
|
||||
*/
|
||||
contract IdentityVerificationHubImplV2 is ImplRoot {
|
||||
|
||||
struct HubStorage {
|
||||
// Existing fields...
|
||||
uint256 newField; // Add new fields at the end only
|
||||
}
|
||||
|
||||
function initialize(...) external reinitializer(13) {
|
||||
// Initialize new fields if needed
|
||||
HubStorage storage $ = _getHubStorage();
|
||||
$.newField = defaultValue;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 2: Run the Upgrade Script
|
||||
|
||||
```bash
|
||||
npx hardhat upgrade --contract <ContractName> --network <network> --changelog "Description"
|
||||
```
|
||||
|
||||
**Options:**
|
||||
|
||||
- `--contract` - Contract name (e.g., `IdentityVerificationHub`)
|
||||
- `--network` - Target network (`celo`, `sepolia`, `localhost`)
|
||||
- `--changelog` - Brief description of changes
|
||||
- `--prepare-only` - Deploy implementation without creating Safe proposal
|
||||
|
||||
### Step 3: Script Execution
|
||||
|
||||
The script automatically:
|
||||
|
||||
1. **Validates version** - Ensures `@custom:version` is incremented correctly
|
||||
2. **Checks reinitializer** - Verifies `reinitializer(N)` matches expected version
|
||||
3. **Validates storage** - Ensures no breaking storage layout changes
|
||||
4. **Compiles fresh** - Clears cache to prevent stale bytecode
|
||||
5. **Compares bytecode** - Warns if implementation hasn't changed
|
||||
6. **Deploys implementation** - Deploys new implementation contract
|
||||
7. **Updates registry** - Records deployment in `deployments/registry.json`
|
||||
8. **Creates git commit & tag** - Auto-commits changes with version tag
|
||||
9. **Creates Safe proposal** - If you're a signer, auto-proposes to Safe
|
||||
|
||||
### Step 4: Multisig Approval
|
||||
|
||||
**If you're a Safe signer:**
|
||||
|
||||
- Script auto-proposes the transaction
|
||||
- Other signers approve in Safe UI
|
||||
- Execute once threshold (3/5) is met
|
||||
|
||||
**If you're not a signer:**
|
||||
|
||||
- Script outputs transaction data for manual submission
|
||||
- Copy data to Safe Transaction Builder
|
||||
- Signers approve and execute
|
||||
|
||||
---
|
||||
|
||||
## Safety Checks
|
||||
|
||||
The upgrade script performs these automatic checks:
|
||||
|
||||
| Check | What it Does | Failure Behavior |
|
||||
| ---------------------- | ---------------------------------- | -------------------- |
|
||||
| Version validation | Ensures semantic version increment | Blocks upgrade |
|
||||
| Reinitializer check | Verifies modifier matches version | Blocks upgrade |
|
||||
| Storage layout | Detects breaking storage changes | Blocks upgrade |
|
||||
| Bytecode comparison | Warns if code unchanged | Prompts confirmation |
|
||||
| Safe role verification | Confirms Safe has `SECURITY_ROLE` | Blocks upgrade |
|
||||
| Constructor check | Flags `_disableInitializers()` | Prompts confirmation |
|
||||
|
||||
---
|
||||
|
||||
## Registry Structure
|
||||
|
||||
All deployments are tracked in `deployments/registry.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"contracts": {
|
||||
"ContractName": {
|
||||
"source": "ContractSourceFile",
|
||||
"type": "uups-proxy"
|
||||
}
|
||||
},
|
||||
"networks": {
|
||||
"celo": {
|
||||
"deployments": {
|
||||
"ContractName": {
|
||||
"proxy": "0x...",
|
||||
"currentVersion": "2.12.0",
|
||||
"currentImpl": "0x..."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"versions": {
|
||||
"ContractName": {
|
||||
"2.12.0": {
|
||||
"initializerVersion": 12,
|
||||
"changelog": "...",
|
||||
"gitTag": "contractname-v2.12.0",
|
||||
"deployments": { ... }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Utility Commands
|
||||
|
||||
```bash
|
||||
# Check current deployment status
|
||||
npx hardhat upgrade:status --contract IdentityVerificationHub --network celo
|
||||
|
||||
# View version history
|
||||
npx hardhat upgrade:history --contract IdentityVerificationHub
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Rollback
|
||||
|
||||
If issues occur after upgrade:
|
||||
|
||||
1. Deploy the previous implementation version
|
||||
2. Create Safe transaction calling `upgradeToAndCall(previousImpl, "0x")`
|
||||
3. Execute with 3/5 multisig approval
|
||||
|
||||
---
|
||||
|
||||
## Environment Setup
|
||||
|
||||
Required in `.env`:
|
||||
|
||||
```bash
|
||||
CELO_RPC_URL=https://forno.celo.org
|
||||
PRIVATE_KEY=0x... # Deployer wallet (needs ETH for gas)
|
||||
```
|
||||
|
||||
Optional for contract verification:
|
||||
|
||||
```bash
|
||||
CELOSCAN_API_KEY=...
|
||||
ETHERSCAN_API_KEY=...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Issue | Solution |
|
||||
| ----------------------------- | ----------------------------------------- |
|
||||
| "Version matches current" | Update `@custom:version` in contract |
|
||||
| "Reinitializer mismatch" | Update `reinitializer(N)` to next version |
|
||||
| "Storage layout incompatible" | Don't remove/reorder storage variables |
|
||||
| "Safe not indexed" | Submit manually via Safe UI |
|
||||
| "Bytecode unchanged" | Ensure you saved contract changes |
|
||||
@@ -12,7 +12,6 @@ import {IIdentityRegistryV1} from "./interfaces/IIdentityRegistryV1.sol";
|
||||
import {IRegisterCircuitVerifier} from "./interfaces/IRegisterCircuitVerifier.sol";
|
||||
import {IVcAndDiscloseCircuitVerifier} from "./interfaces/IVcAndDiscloseCircuitVerifier.sol";
|
||||
import {IDscCircuitVerifier} from "./interfaces/IDscCircuitVerifier.sol";
|
||||
import {ImplRoot} from "./upgradeable/ImplRoot.sol";
|
||||
|
||||
/**
|
||||
* @notice ⚠️ CRITICAL STORAGE LAYOUT WARNING ⚠️
|
||||
@@ -43,9 +42,12 @@ import {ImplRoot} from "./upgradeable/ImplRoot.sol";
|
||||
/**
|
||||
* @title IdentityVerificationHubStorageV1
|
||||
* @notice Storage contract for IdentityVerificationHubImplV1.
|
||||
* @dev Inherits from ImplRoot to include upgradeability functionality.
|
||||
* @dev Inherits from UUPSUpgradeable and Ownable2StepUpgradeable to include upgradeability functionality.
|
||||
*/
|
||||
abstract contract IdentityVerificationHubStorageV1 is ImplRoot {
|
||||
abstract contract IdentityVerificationHubStorageV1 is UUPSUpgradeable, Ownable2StepUpgradeable {
|
||||
// Reserved storage space to allow for layout changes in the future.
|
||||
uint256[50] private __gap;
|
||||
|
||||
// ====================================================
|
||||
// Storage Variables
|
||||
// ====================================================
|
||||
@@ -61,6 +63,14 @@ abstract contract IdentityVerificationHubStorageV1 is ImplRoot {
|
||||
|
||||
/// @notice Mapping from signature type to DSC circuit verifier addresses..
|
||||
mapping(uint256 => address) internal _sigTypeToDscCircuitVerifiers;
|
||||
|
||||
/**
|
||||
* @dev Authorizes an upgrade to a new implementation.
|
||||
* Requirements:
|
||||
* - Must be called through a proxy.
|
||||
* - Caller must be the owner.
|
||||
*/
|
||||
function _authorizeUpgrade(address newImplementation) internal virtual override onlyProxy onlyOwner {}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -207,7 +217,7 @@ contract IdentityVerificationHubImplV1 is IdentityVerificationHubStorageV1, IIde
|
||||
uint256[] memory dscCircuitVerifierIds,
|
||||
address[] memory dscCircuitVerifierAddresses
|
||||
) external initializer {
|
||||
__ImplRoot_init();
|
||||
__Ownable_init(msg.sender);
|
||||
_registry = registryAddress;
|
||||
_vcAndDiscloseCircuitVerifier = vcAndDiscloseCircuitVerifierAddress;
|
||||
if (registerCircuitVerifierIds.length != registerCircuitVerifierAddresses.length) {
|
||||
|
||||
@@ -19,6 +19,14 @@ import {IDscCircuitVerifier} from "./interfaces/IDscCircuitVerifier.sol";
|
||||
import {CircuitConstantsV2} from "./constants/CircuitConstantsV2.sol";
|
||||
import {Formatter} from "./libraries/Formatter.sol";
|
||||
|
||||
/**
|
||||
* @title IdentityVerificationHubImplV2
|
||||
* @notice Main hub for identity verification in the Self Protocol
|
||||
* @dev This contract orchestrates multi-step verification processes including document attestation,
|
||||
* zero-knowledge proofs, OFAC compliance, and attribute disclosure control.
|
||||
*
|
||||
* @custom:version 2.12.0
|
||||
*/
|
||||
contract IdentityVerificationHubImplV2 is ImplRoot {
|
||||
/// @custom:storage-location erc7201:self.storage.IdentityVerificationHub
|
||||
struct IdentityVerificationHubStorage {
|
||||
@@ -45,7 +53,7 @@ contract IdentityVerificationHubImplV2 is ImplRoot {
|
||||
0xf9b5980dcec1a8b0609576a1f453bb2cad4732a0ea02bb89154d44b14a306c00;
|
||||
|
||||
/// @notice The AADHAAR registration window around the current block timestamp.
|
||||
uint256 public AADHAAR_REGISTRATION_WINDOW = 20;
|
||||
uint256 public AADHAAR_REGISTRATION_WINDOW;
|
||||
|
||||
/**
|
||||
* @notice Returns the storage struct for the main IdentityVerificationHub.
|
||||
@@ -218,6 +226,7 @@ contract IdentityVerificationHubImplV2 is ImplRoot {
|
||||
* @notice Constructor that disables initializers for the implementation contract.
|
||||
* @dev This prevents the implementation contract from being initialized directly.
|
||||
* The actual initialization should only happen through the proxy.
|
||||
* @custom:oz-upgrades-unsafe-allow constructor
|
||||
*/
|
||||
constructor() {
|
||||
_disableInitializers();
|
||||
@@ -240,9 +249,25 @@ contract IdentityVerificationHubImplV2 is ImplRoot {
|
||||
IdentityVerificationHubStorage storage $ = _getIdentityVerificationHubStorage();
|
||||
$._circuitVersion = 2;
|
||||
|
||||
// Initialize Aadhaar registration window
|
||||
AADHAAR_REGISTRATION_WINDOW = 20;
|
||||
|
||||
emit HubInitializedV2();
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Initializes governance for upgraded contracts.
|
||||
* @dev Used when upgrading from Ownable to AccessControl governance.
|
||||
* This function sets up AccessControl roles on an already-initialized contract.
|
||||
* It does NOT modify existing state (hub, roots, etc.).
|
||||
*
|
||||
* SECURITY: This function can only be called once - enforced by reinitializer(12).
|
||||
* The previous version used reinitializer(11), so this upgrade uses version 12.
|
||||
*/
|
||||
function initializeGovernance() external reinitializer(12) {
|
||||
__ImplRoot_init();
|
||||
}
|
||||
|
||||
// ====================================================
|
||||
// External Functions
|
||||
// ====================================================
|
||||
@@ -329,7 +354,7 @@ contract IdentityVerificationHubImplV2 is ImplRoot {
|
||||
* @notice Updates the AADHAAR registration window.
|
||||
* @param window The new AADHAAR registration window.
|
||||
*/
|
||||
function setAadhaarRegistrationWindow(uint256 window) external virtual onlyProxy onlyOwner {
|
||||
function setAadhaarRegistrationWindow(uint256 window) external virtual onlyProxy onlyRole(SECURITY_ROLE) {
|
||||
AADHAAR_REGISTRATION_WINDOW = window;
|
||||
}
|
||||
|
||||
@@ -372,7 +397,10 @@ contract IdentityVerificationHubImplV2 is ImplRoot {
|
||||
* @notice Updates the registry address.
|
||||
* @param registryAddress The new registry address.
|
||||
*/
|
||||
function updateRegistry(bytes32 attestationId, address registryAddress) external virtual onlyProxy onlyOwner {
|
||||
function updateRegistry(
|
||||
bytes32 attestationId,
|
||||
address registryAddress
|
||||
) external virtual onlyProxy onlyRole(SECURITY_ROLE) {
|
||||
IdentityVerificationHubStorage storage $ = _getIdentityVerificationHubStorage();
|
||||
$._registries[attestationId] = registryAddress;
|
||||
emit RegistryUpdated(attestationId, registryAddress);
|
||||
@@ -385,7 +413,7 @@ contract IdentityVerificationHubImplV2 is ImplRoot {
|
||||
function updateVcAndDiscloseCircuit(
|
||||
bytes32 attestationId,
|
||||
address vcAndDiscloseCircuitVerifierAddress
|
||||
) external virtual onlyProxy onlyOwner {
|
||||
) external virtual onlyProxy onlyRole(SECURITY_ROLE) {
|
||||
IdentityVerificationHubStorage storage $ = _getIdentityVerificationHubStorage();
|
||||
$._discloseVerifiers[attestationId] = vcAndDiscloseCircuitVerifierAddress;
|
||||
emit VcAndDiscloseCircuitUpdated(attestationId, vcAndDiscloseCircuitVerifierAddress);
|
||||
@@ -401,7 +429,7 @@ contract IdentityVerificationHubImplV2 is ImplRoot {
|
||||
bytes32 attestationId,
|
||||
uint256 typeId,
|
||||
address verifierAddress
|
||||
) external virtual onlyProxy onlyOwner {
|
||||
) external virtual onlyProxy onlyRole(SECURITY_ROLE) {
|
||||
IdentityVerificationHubStorage storage $ = _getIdentityVerificationHubStorage();
|
||||
$._registerCircuitVerifiers[attestationId][typeId] = verifierAddress;
|
||||
emit RegisterCircuitVerifierUpdated(typeId, verifierAddress);
|
||||
@@ -417,7 +445,7 @@ contract IdentityVerificationHubImplV2 is ImplRoot {
|
||||
bytes32 attestationId,
|
||||
uint256 typeId,
|
||||
address verifierAddress
|
||||
) external virtual onlyProxy onlyOwner {
|
||||
) external virtual onlyProxy onlyRole(SECURITY_ROLE) {
|
||||
IdentityVerificationHubStorage storage $ = _getIdentityVerificationHubStorage();
|
||||
$._dscCircuitVerifiers[attestationId][typeId] = verifierAddress;
|
||||
emit DscCircuitVerifierUpdated(typeId, verifierAddress);
|
||||
@@ -433,7 +461,7 @@ contract IdentityVerificationHubImplV2 is ImplRoot {
|
||||
bytes32[] calldata attestationIds,
|
||||
uint256[] calldata typeIds,
|
||||
address[] calldata verifierAddresses
|
||||
) external virtual onlyProxy onlyOwner {
|
||||
) external virtual onlyProxy onlyRole(SECURITY_ROLE) {
|
||||
if (attestationIds.length != typeIds.length || attestationIds.length != verifierAddresses.length) {
|
||||
revert LengthMismatch();
|
||||
}
|
||||
@@ -454,7 +482,7 @@ contract IdentityVerificationHubImplV2 is ImplRoot {
|
||||
bytes32[] calldata attestationIds,
|
||||
uint256[] calldata typeIds,
|
||||
address[] calldata verifierAddresses
|
||||
) external virtual onlyProxy onlyOwner {
|
||||
) external virtual onlyProxy onlyRole(SECURITY_ROLE) {
|
||||
if (attestationIds.length != typeIds.length || attestationIds.length != verifierAddresses.length) {
|
||||
revert LengthMismatch();
|
||||
}
|
||||
@@ -677,15 +705,11 @@ contract IdentityVerificationHubImplV2 is ImplRoot {
|
||||
_performUserIdentifierCheck(userContextData, vcAndDiscloseProof, header.attestationId, indices);
|
||||
}
|
||||
|
||||
// Scope 2: Root and date checks
|
||||
// Scope 2: Root, OFAC, and current date checks
|
||||
{
|
||||
_performRootCheck(header.attestationId, vcAndDiscloseProof, indices);
|
||||
_performOfacCheck(header.attestationId, vcAndDiscloseProof, indices);
|
||||
if (header.attestationId == AttestationId.AADHAAR) {
|
||||
_performNumericCurrentDateCheck(vcAndDiscloseProof, indices);
|
||||
} else {
|
||||
_performCurrentDateCheck(vcAndDiscloseProof, indices);
|
||||
}
|
||||
_performCurrentDateCheck(header.attestationId, vcAndDiscloseProof, indices);
|
||||
}
|
||||
|
||||
// Scope 3: Groth16 proof verification
|
||||
@@ -981,41 +1005,56 @@ contract IdentityVerificationHubImplV2 is ImplRoot {
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Performs current date validation
|
||||
* @notice Performs current date validation with format-aware parsing
|
||||
* @dev Handles three date formats:
|
||||
* - E_PASSPORT/EU_ID_CARD: 6 ASCII chars (YYMMDD)
|
||||
* - SELFRICA_ID_CARD: 8 ASCII digits (YYYYMMDD)
|
||||
* - AADHAAR: 3 numeric signals (year, month, day)
|
||||
* @param attestationId The attestation type to determine date format
|
||||
* @param vcAndDiscloseProof The proof containing date information
|
||||
* @param indices Circuit-specific indices for extracting date values
|
||||
*/
|
||||
function _performCurrentDateCheck(
|
||||
bytes32 attestationId,
|
||||
GenericProofStruct memory vcAndDiscloseProof,
|
||||
CircuitConstantsV2.DiscloseIndices memory indices
|
||||
) internal view {
|
||||
uint256[6] memory dateNum;
|
||||
for (uint256 i = 0; i < 6; i++) {
|
||||
dateNum[i] = vcAndDiscloseProof.pubSignals[indices.currentDateIndex + i];
|
||||
uint256 currentTimestamp;
|
||||
uint256 startIndex = indices.currentDateIndex;
|
||||
|
||||
if (attestationId == AttestationId.E_PASSPORT || attestationId == AttestationId.EU_ID_CARD) {
|
||||
// E_PASSPORT, EU_ID_CARD: 6 ASCII chars (YYMMDD)
|
||||
uint256[6] memory dateNum;
|
||||
unchecked {
|
||||
for (uint256 i; i < 6; ++i) {
|
||||
dateNum[i] = vcAndDiscloseProof.pubSignals[startIndex + i];
|
||||
}
|
||||
}
|
||||
currentTimestamp = Formatter.proofDateToUnixTimestamp(dateNum);
|
||||
} else {
|
||||
// AADHAAR: 3 numeric signals [year, month, day]
|
||||
currentTimestamp = Formatter.proofDateToUnixTimestampNumeric(
|
||||
[
|
||||
vcAndDiscloseProof.pubSignals[startIndex],
|
||||
vcAndDiscloseProof.pubSignals[startIndex + 1],
|
||||
vcAndDiscloseProof.pubSignals[startIndex + 2]
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
uint256 currentTimestamp = Formatter.proofDateToUnixTimestamp(dateNum);
|
||||
uint256 startOfDay = _getStartOfDayTimestamp();
|
||||
uint256 endOfDay = startOfDay + 1 days - 1;
|
||||
|
||||
if (currentTimestamp < startOfDay - 1 days + 1 || currentTimestamp > endOfDay + 1 days) {
|
||||
revert CurrentDateNotInValidRange();
|
||||
}
|
||||
_validateDateInRange(currentTimestamp);
|
||||
}
|
||||
|
||||
function _performNumericCurrentDateCheck(
|
||||
GenericProofStruct memory vcAndDiscloseProof,
|
||||
CircuitConstantsV2.DiscloseIndices memory indices
|
||||
) internal view {
|
||||
// date is going to be 2025, 12, 13
|
||||
uint256[3] memory dateNum;
|
||||
dateNum[0] = vcAndDiscloseProof.pubSignals[indices.currentDateIndex];
|
||||
dateNum[1] = vcAndDiscloseProof.pubSignals[indices.currentDateIndex + 1];
|
||||
dateNum[2] = vcAndDiscloseProof.pubSignals[indices.currentDateIndex + 2];
|
||||
/**
|
||||
* @notice Validates that a timestamp is within the acceptable range
|
||||
* @param currentTimestamp The timestamp to validate
|
||||
*/
|
||||
function _validateDateInRange(uint256 currentTimestamp) internal view {
|
||||
// Calculate the timestamp for the start of current date by subtracting the remainder of block.timestamp modulo 1 day
|
||||
uint256 startOfDay = block.timestamp - (block.timestamp % 1 days);
|
||||
|
||||
uint256 currentTimestamp = Formatter.proofDateToUnixTimestampNumeric(dateNum);
|
||||
uint256 startOfDay = _getStartOfDayTimestamp();
|
||||
uint256 endOfDay = startOfDay + 1 days - 1;
|
||||
|
||||
if (currentTimestamp < startOfDay - 1 days + 1 || currentTimestamp > endOfDay + 1 days) {
|
||||
// Check if timestamp is within range
|
||||
if (currentTimestamp < startOfDay - 1 days + 1 || currentTimestamp > startOfDay + 1 days - 1) {
|
||||
revert CurrentDateNotInValidRange();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,3 +49,21 @@ interface IVcAndDiscloseAadhaarCircuitVerifier {
|
||||
uint256[19] calldata pubSignals
|
||||
) external view returns (bool);
|
||||
}
|
||||
|
||||
interface IVcAndDiscloseSelfricaCircuitVerifier {
|
||||
/**
|
||||
* @notice Verifies a given VC and Disclose zero-knowledge proof.
|
||||
* @dev This function checks the validity of the provided proof parameters.
|
||||
* @param a The 'a' component of the proof.
|
||||
* @param b The 'b' component of the proof.
|
||||
* @param c The 'c' component of the proof.
|
||||
* @param pubSignals The public signals associated with the proof.
|
||||
* @return A boolean value indicating whether the proof is valid (true) or not (false).
|
||||
*/
|
||||
function verifyProof(
|
||||
uint256[2] calldata a,
|
||||
uint256[2][2] calldata b,
|
||||
uint256[2] calldata c,
|
||||
uint256[30] calldata pubSignals
|
||||
) external view returns (bool);
|
||||
}
|
||||
|
||||
@@ -70,6 +70,8 @@ abstract contract IdentityRegistryAadhaarStorageV1 is ImplRoot {
|
||||
* @title IdentityRegistryAadhaarImplV1
|
||||
* @notice Provides functions to register and manage identity commitments using a Merkle tree structure.
|
||||
* @dev Inherits from IdentityRegistryAadhaarStorageV1 and implements IIdentityRegistryAadhaarV1.
|
||||
*
|
||||
* @custom:version 1.2.0
|
||||
*/
|
||||
contract IdentityRegistryAadhaarImplV1 is IdentityRegistryAadhaarStorageV1, IIdentityRegistryAadhaarV1 {
|
||||
using InternalLeanIMT for LeanIMTData;
|
||||
@@ -151,6 +153,7 @@ contract IdentityRegistryAadhaarImplV1 is IdentityRegistryAadhaarStorageV1, IIde
|
||||
// ====================================================
|
||||
|
||||
/// @notice Constructor for the IdentityRegistryAadhaarImplV1 contract.
|
||||
/// @custom:oz-upgrades-unsafe-allow constructor
|
||||
constructor() {
|
||||
_disableInitializers();
|
||||
}
|
||||
@@ -168,6 +171,19 @@ contract IdentityRegistryAadhaarImplV1 is IdentityRegistryAadhaarStorageV1, IIde
|
||||
emit RegistryInitialized(_hub);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Initializes AccessControl governance.
|
||||
* @dev Used when upgrading from Ownable to AccessControl governance.
|
||||
* This function sets up AccessControl roles on an already-initialized contract.
|
||||
* It does NOT modify existing state (hub, roots, etc.).
|
||||
*
|
||||
* SECURITY: This function can only be called once - enforced by reinitializer(2).
|
||||
* The previous version used reinitializer(1), so this upgrade uses version 2.
|
||||
*/
|
||||
function initializeGovernance() external reinitializer(2) {
|
||||
__ImplRoot_init();
|
||||
}
|
||||
|
||||
// ====================================================
|
||||
// External Functions - View & Checks
|
||||
// ====================================================
|
||||
@@ -280,7 +296,7 @@ contract IdentityRegistryAadhaarImplV1 is IdentityRegistryAadhaarStorageV1, IIde
|
||||
/// @notice Updates the hub address.
|
||||
/// @dev Callable only via a proxy and restricted to the contract owner.
|
||||
/// @param newHubAddress The new address of the hub.
|
||||
function updateHub(address newHubAddress) external onlyProxy onlyOwner {
|
||||
function updateHub(address newHubAddress) external onlyProxy onlyRole(SECURITY_ROLE) {
|
||||
if (newHubAddress == address(0)) revert HUB_ADDRESS_ZERO();
|
||||
_hub = newHubAddress;
|
||||
emit HubUpdated(newHubAddress);
|
||||
@@ -289,7 +305,7 @@ contract IdentityRegistryAadhaarImplV1 is IdentityRegistryAadhaarStorageV1, IIde
|
||||
/// @notice Updates the name and date of birth OFAC root.
|
||||
/// @dev Callable only via a proxy and restricted to the contract owner.
|
||||
/// @param newNameAndDobOfacRoot The new name and date of birth OFAC root value.
|
||||
function updateNameAndDobOfacRoot(uint256 newNameAndDobOfacRoot) external onlyProxy onlyOwner {
|
||||
function updateNameAndDobOfacRoot(uint256 newNameAndDobOfacRoot) external onlyProxy onlyRole(OPERATIONS_ROLE) {
|
||||
_nameAndDobOfacRoot = newNameAndDobOfacRoot;
|
||||
emit NameAndDobOfacRootUpdated(newNameAndDobOfacRoot);
|
||||
}
|
||||
@@ -297,7 +313,7 @@ contract IdentityRegistryAadhaarImplV1 is IdentityRegistryAadhaarStorageV1, IIde
|
||||
/// @notice Updates the name and year of birth OFAC root.
|
||||
/// @dev Callable only via a proxy and restricted to the contract owner.
|
||||
/// @param newNameAndYobOfacRoot The new name and year of birth OFAC root value.
|
||||
function updateNameAndYobOfacRoot(uint256 newNameAndYobOfacRoot) external onlyProxy onlyOwner {
|
||||
function updateNameAndYobOfacRoot(uint256 newNameAndYobOfacRoot) external onlyProxy onlyRole(OPERATIONS_ROLE) {
|
||||
_nameAndYobOfacRoot = newNameAndYobOfacRoot;
|
||||
emit NameAndYobOfacRootUpdated(newNameAndYobOfacRoot);
|
||||
}
|
||||
@@ -305,7 +321,7 @@ contract IdentityRegistryAadhaarImplV1 is IdentityRegistryAadhaarStorageV1, IIde
|
||||
/// @notice Registers a new UIDAI pubkey commitment.
|
||||
/// @dev Callable only via a proxy and restricted to the contract owner.
|
||||
/// @param commitment The UIDAI pubkey commitment to register.
|
||||
function registerUidaiPubkeyCommitment(uint256 commitment) external onlyProxy onlyOwner {
|
||||
function registerUidaiPubkeyCommitment(uint256 commitment) external onlyProxy onlyRole(SECURITY_ROLE) {
|
||||
_uidaiPubkeyCommitments[commitment] = true;
|
||||
emit UidaiPubkeyCommitmentRegistered(commitment, block.timestamp);
|
||||
}
|
||||
@@ -313,7 +329,7 @@ contract IdentityRegistryAadhaarImplV1 is IdentityRegistryAadhaarStorageV1, IIde
|
||||
/// @notice Removes a UIDAI pubkey commitment.
|
||||
/// @dev Callable only via a proxy and restricted to the contract owner.
|
||||
/// @param commitment The UIDAI pubkey commitment to remove.
|
||||
function removeUidaiPubkeyCommitment(uint256 commitment) external onlyProxy onlyOwner {
|
||||
function removeUidaiPubkeyCommitment(uint256 commitment) external onlyProxy onlyRole(SECURITY_ROLE) {
|
||||
delete _uidaiPubkeyCommitments[commitment];
|
||||
emit UidaiPubkeyCommitmentRemoved(commitment, block.timestamp);
|
||||
}
|
||||
@@ -321,7 +337,7 @@ contract IdentityRegistryAadhaarImplV1 is IdentityRegistryAadhaarStorageV1, IIde
|
||||
/// @notice Updates a UIDAI pubkey commitment.
|
||||
/// @dev Callable only via a proxy and restricted to the contract owner.
|
||||
/// @param commitment The UIDAI pubkey commitment to update.
|
||||
function updateUidaiPubkeyCommitment(uint256 commitment) external onlyProxy onlyOwner {
|
||||
function updateUidaiPubkeyCommitment(uint256 commitment) external onlyProxy onlyRole(SECURITY_ROLE) {
|
||||
_uidaiPubkeyCommitments[commitment] = true;
|
||||
emit UidaiPubkeyCommitmentUpdated(commitment, block.timestamp);
|
||||
}
|
||||
@@ -335,7 +351,7 @@ contract IdentityRegistryAadhaarImplV1 is IdentityRegistryAadhaarStorageV1, IIde
|
||||
bytes32 attestationId,
|
||||
uint256 nullifier,
|
||||
uint256 commitment
|
||||
) external onlyProxy onlyOwner {
|
||||
) external onlyProxy onlyRole(SECURITY_ROLE) {
|
||||
_nullifiers[nullifier] = true;
|
||||
uint256 imt_root = _identityCommitmentIMT._insert(commitment);
|
||||
_rootTimestamps[imt_root] = block.timestamp;
|
||||
@@ -352,7 +368,7 @@ contract IdentityRegistryAadhaarImplV1 is IdentityRegistryAadhaarStorageV1, IIde
|
||||
uint256 oldLeaf,
|
||||
uint256 newLeaf,
|
||||
uint256[] calldata siblingNodes
|
||||
) external onlyProxy onlyOwner {
|
||||
) external onlyProxy onlyRole(SECURITY_ROLE) {
|
||||
uint256 imt_root = _identityCommitmentIMT._update(oldLeaf, newLeaf, siblingNodes);
|
||||
_rootTimestamps[imt_root] = block.timestamp;
|
||||
emit DevCommitmentUpdated(oldLeaf, newLeaf, imt_root, block.timestamp);
|
||||
@@ -362,7 +378,10 @@ contract IdentityRegistryAadhaarImplV1 is IdentityRegistryAadhaarStorageV1, IIde
|
||||
/// @dev Caller must be the owner. Provides sibling nodes for proof of position.
|
||||
/// @param oldLeaf The identity commitment to remove.
|
||||
/// @param siblingNodes An array of sibling nodes for Merkle proof generation.
|
||||
function devRemoveCommitment(uint256 oldLeaf, uint256[] calldata siblingNodes) external onlyProxy onlyOwner {
|
||||
function devRemoveCommitment(
|
||||
uint256 oldLeaf,
|
||||
uint256[] calldata siblingNodes
|
||||
) external onlyProxy onlyRole(SECURITY_ROLE) {
|
||||
uint256 imt_root = _identityCommitmentIMT._remove(oldLeaf, siblingNodes);
|
||||
_rootTimestamps[imt_root] = block.timestamp;
|
||||
emit DevCommitmentRemoved(oldLeaf, imt_root, block.timestamp);
|
||||
|
||||
@@ -77,6 +77,8 @@ abstract contract IdentityRegistryIdCardStorageV1 is ImplRoot {
|
||||
* @title IdentityRegistryImplV1
|
||||
* @notice Provides functions to register and manage identity commitments using a Merkle tree structure.
|
||||
* @dev Inherits from IdentityRegistryStorageV1 and implements IIdentityRegistryV1.
|
||||
*
|
||||
* @custom:version 1.2.0
|
||||
*/
|
||||
contract IdentityRegistryIdCardImplV1 is IdentityRegistryIdCardStorageV1, IIdentityRegistryIdCardV1 {
|
||||
using InternalLeanIMT for LeanIMTData;
|
||||
@@ -162,6 +164,7 @@ contract IdentityRegistryIdCardImplV1 is IdentityRegistryIdCardStorageV1, IIdent
|
||||
/**
|
||||
* @notice Constructor that disables initializers.
|
||||
* @dev Prevents direct initialization of the implementation contract.
|
||||
* @custom:oz-upgrades-unsafe-allow constructor
|
||||
*/
|
||||
constructor() {
|
||||
_disableInitializers();
|
||||
@@ -181,6 +184,19 @@ contract IdentityRegistryIdCardImplV1 is IdentityRegistryIdCardStorageV1, IIdent
|
||||
emit RegistryInitialized(_hub);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Initializes governance for upgraded contracts.
|
||||
* @dev Used when upgrading from Ownable to AccessControl governance.
|
||||
* This function sets up AccessControl roles on an already-initialized contract.
|
||||
* It does NOT modify existing state (hub, roots, etc.).
|
||||
*
|
||||
* SECURITY: This function can only be called once - enforced by reinitializer(2).
|
||||
* The previous version used reinitializer(1), so this upgrade uses version 2.
|
||||
*/
|
||||
function initializeGovernance() external reinitializer(2) {
|
||||
__ImplRoot_init();
|
||||
}
|
||||
|
||||
// ====================================================
|
||||
// External Functions - View & Checks
|
||||
// ====================================================
|
||||
@@ -380,7 +396,7 @@ contract IdentityRegistryIdCardImplV1 is IdentityRegistryIdCardStorageV1, IIdent
|
||||
* @dev Callable only via a proxy and restricted to the contract owner.
|
||||
* @param newHubAddress The new address of the hub.
|
||||
*/
|
||||
function updateHub(address newHubAddress) external onlyProxy onlyOwner {
|
||||
function updateHub(address newHubAddress) external onlyProxy onlyRole(SECURITY_ROLE) {
|
||||
_hub = newHubAddress;
|
||||
emit HubUpdated(newHubAddress);
|
||||
}
|
||||
@@ -390,7 +406,7 @@ contract IdentityRegistryIdCardImplV1 is IdentityRegistryIdCardStorageV1, IIdent
|
||||
* @dev Callable only via a proxy and restricted to the contract owner.
|
||||
* @param newNameAndDobOfacRoot The new name and date of birth OFAC root value.
|
||||
*/
|
||||
function updateNameAndDobOfacRoot(uint256 newNameAndDobOfacRoot) external onlyProxy onlyOwner {
|
||||
function updateNameAndDobOfacRoot(uint256 newNameAndDobOfacRoot) external onlyProxy onlyRole(OPERATIONS_ROLE) {
|
||||
_nameAndDobOfacRoot = newNameAndDobOfacRoot;
|
||||
emit NameAndDobOfacRootUpdated(newNameAndDobOfacRoot);
|
||||
}
|
||||
@@ -400,7 +416,7 @@ contract IdentityRegistryIdCardImplV1 is IdentityRegistryIdCardStorageV1, IIdent
|
||||
* @dev Callable only via a proxy and restricted to the contract owner.
|
||||
* @param newNameAndYobOfacRoot The new name and year of birth OFAC root value.
|
||||
*/
|
||||
function updateNameAndYobOfacRoot(uint256 newNameAndYobOfacRoot) external onlyProxy onlyOwner {
|
||||
function updateNameAndYobOfacRoot(uint256 newNameAndYobOfacRoot) external onlyProxy onlyRole(OPERATIONS_ROLE) {
|
||||
_nameAndYobOfacRoot = newNameAndYobOfacRoot;
|
||||
emit NameAndYobOfacRootUpdated(newNameAndYobOfacRoot);
|
||||
}
|
||||
@@ -410,7 +426,7 @@ contract IdentityRegistryIdCardImplV1 is IdentityRegistryIdCardStorageV1, IIdent
|
||||
* @dev Callable only via a proxy and restricted to the contract owner.
|
||||
* @param newCscaRoot The new CSCA root value.
|
||||
*/
|
||||
function updateCscaRoot(uint256 newCscaRoot) external onlyProxy onlyOwner {
|
||||
function updateCscaRoot(uint256 newCscaRoot) external onlyProxy onlyRole(OPERATIONS_ROLE) {
|
||||
_cscaRoot = newCscaRoot;
|
||||
emit CscaRootUpdated(newCscaRoot);
|
||||
}
|
||||
@@ -426,7 +442,7 @@ contract IdentityRegistryIdCardImplV1 is IdentityRegistryIdCardStorageV1, IIdent
|
||||
bytes32 attestationId,
|
||||
uint256 nullifier,
|
||||
uint256 commitment
|
||||
) external onlyProxy onlyOwner {
|
||||
) external onlyProxy onlyRole(SECURITY_ROLE) {
|
||||
_nullifiers[attestationId][nullifier] = true;
|
||||
uint256 imt_root = _addCommitment(_identityCommitmentIMT, commitment);
|
||||
_rootTimestamps[imt_root] = block.timestamp;
|
||||
@@ -445,7 +461,7 @@ contract IdentityRegistryIdCardImplV1 is IdentityRegistryIdCardStorageV1, IIdent
|
||||
uint256 oldLeaf,
|
||||
uint256 newLeaf,
|
||||
uint256[] calldata siblingNodes
|
||||
) external onlyProxy onlyOwner {
|
||||
) external onlyProxy onlyRole(SECURITY_ROLE) {
|
||||
uint256 imt_root = _updateCommitment(_identityCommitmentIMT, oldLeaf, newLeaf, siblingNodes);
|
||||
_rootTimestamps[imt_root] = block.timestamp;
|
||||
emit DevCommitmentUpdated(oldLeaf, newLeaf, imt_root, block.timestamp);
|
||||
@@ -457,7 +473,10 @@ contract IdentityRegistryIdCardImplV1 is IdentityRegistryIdCardStorageV1, IIdent
|
||||
* @param oldLeaf The identity commitment to remove.
|
||||
* @param siblingNodes An array of sibling nodes for Merkle proof generation.
|
||||
*/
|
||||
function devRemoveCommitment(uint256 oldLeaf, uint256[] calldata siblingNodes) external onlyProxy onlyOwner {
|
||||
function devRemoveCommitment(
|
||||
uint256 oldLeaf,
|
||||
uint256[] calldata siblingNodes
|
||||
) external onlyProxy onlyRole(SECURITY_ROLE) {
|
||||
uint256 imt_root = _removeCommitment(_identityCommitmentIMT, oldLeaf, siblingNodes);
|
||||
_rootTimestamps[imt_root] = block.timestamp;
|
||||
emit DevCommitmentRemoved(oldLeaf, imt_root, block.timestamp);
|
||||
@@ -468,7 +487,7 @@ contract IdentityRegistryIdCardImplV1 is IdentityRegistryIdCardStorageV1, IIdent
|
||||
* @dev Callable only by the owner for testing or administration.
|
||||
* @param dscCommitment The DSC key commitment to add.
|
||||
*/
|
||||
function devAddDscKeyCommitment(uint256 dscCommitment) external onlyProxy onlyOwner {
|
||||
function devAddDscKeyCommitment(uint256 dscCommitment) external onlyProxy onlyRole(SECURITY_ROLE) {
|
||||
_isRegisteredDscKeyCommitment[dscCommitment] = true;
|
||||
uint256 imt_root = _addCommitment(_dscKeyCommitmentIMT, dscCommitment);
|
||||
uint256 index = _dscKeyCommitmentIMT._indexOf(dscCommitment);
|
||||
@@ -486,7 +505,7 @@ contract IdentityRegistryIdCardImplV1 is IdentityRegistryIdCardStorageV1, IIdent
|
||||
uint256 oldLeaf,
|
||||
uint256 newLeaf,
|
||||
uint256[] calldata siblingNodes
|
||||
) external onlyProxy onlyOwner {
|
||||
) external onlyProxy onlyRole(SECURITY_ROLE) {
|
||||
uint256 imt_root = _updateCommitment(_dscKeyCommitmentIMT, oldLeaf, newLeaf, siblingNodes);
|
||||
emit DevDscKeyCommitmentUpdated(oldLeaf, newLeaf, imt_root);
|
||||
}
|
||||
@@ -497,7 +516,10 @@ contract IdentityRegistryIdCardImplV1 is IdentityRegistryIdCardStorageV1, IIdent
|
||||
* @param oldLeaf The DSC key commitment to remove.
|
||||
* @param siblingNodes An array of sibling nodes for Merkle proof generation.
|
||||
*/
|
||||
function devRemoveDscKeyCommitment(uint256 oldLeaf, uint256[] calldata siblingNodes) external onlyProxy onlyOwner {
|
||||
function devRemoveDscKeyCommitment(
|
||||
uint256 oldLeaf,
|
||||
uint256[] calldata siblingNodes
|
||||
) external onlyProxy onlyRole(SECURITY_ROLE) {
|
||||
uint256 imt_root = _removeCommitment(_dscKeyCommitmentIMT, oldLeaf, siblingNodes);
|
||||
emit DevDscKeyCommitmentRemoved(oldLeaf, imt_root);
|
||||
}
|
||||
@@ -513,7 +535,7 @@ contract IdentityRegistryIdCardImplV1 is IdentityRegistryIdCardStorageV1, IIdent
|
||||
bytes32 attestationId,
|
||||
uint256 nullifier,
|
||||
bool state
|
||||
) external onlyProxy onlyOwner {
|
||||
) external onlyProxy onlyRole(SECURITY_ROLE) {
|
||||
_nullifiers[attestationId][nullifier] = state;
|
||||
emit DevNullifierStateChanged(attestationId, nullifier, state);
|
||||
}
|
||||
@@ -524,7 +546,10 @@ contract IdentityRegistryIdCardImplV1 is IdentityRegistryIdCardStorageV1, IIdent
|
||||
* @param dscCommitment The DSC key commitment.
|
||||
* @param state The new state of the DSC key commitment (true for registered, false for not registered).
|
||||
*/
|
||||
function devChangeDscKeyCommitmentState(uint256 dscCommitment, bool state) external onlyProxy onlyOwner {
|
||||
function devChangeDscKeyCommitmentState(
|
||||
uint256 dscCommitment,
|
||||
bool state
|
||||
) external onlyProxy onlyRole(SECURITY_ROLE) {
|
||||
_isRegisteredDscKeyCommitment[dscCommitment] = state;
|
||||
emit DevDscKeyCommitmentStateChanged(dscCommitment, state);
|
||||
}
|
||||
|
||||
@@ -82,6 +82,8 @@ abstract contract IdentityRegistryStorageV1 is ImplRoot {
|
||||
* @title IdentityRegistryImplV1
|
||||
* @notice Provides functions to register and manage identity commitments using a Merkle tree structure.
|
||||
* @dev Inherits from IdentityRegistryStorageV1 and implements IIdentityRegistryV1.
|
||||
*
|
||||
* @custom:version 1.2.0
|
||||
*/
|
||||
contract IdentityRegistryImplV1 is IdentityRegistryStorageV1, IIdentityRegistryV1 {
|
||||
using InternalLeanIMT for LeanIMTData;
|
||||
@@ -169,6 +171,7 @@ contract IdentityRegistryImplV1 is IdentityRegistryStorageV1, IIdentityRegistryV
|
||||
/**
|
||||
* @notice Constructor that disables initializers.
|
||||
* @dev Prevents direct initialization of the implementation contract.
|
||||
* @custom:oz-upgrades-unsafe-allow constructor
|
||||
*/
|
||||
constructor() {
|
||||
_disableInitializers();
|
||||
@@ -188,6 +191,19 @@ contract IdentityRegistryImplV1 is IdentityRegistryStorageV1, IIdentityRegistryV
|
||||
emit RegistryInitialized(hubAddress);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Initializes governance for upgraded contracts.
|
||||
* @dev Used when upgrading from Ownable to AccessControl governance.
|
||||
* This function sets up AccessControl roles on an already-initialized contract.
|
||||
* It does NOT modify existing state (hub, roots, etc.).
|
||||
*
|
||||
* SECURITY: This function can only be called once - enforced by reinitializer(2).
|
||||
* The previous version used reinitializer(1), so this upgrade uses version 2.
|
||||
*/
|
||||
function initializeGovernance() external reinitializer(2) {
|
||||
__ImplRoot_init();
|
||||
}
|
||||
|
||||
// ====================================================
|
||||
// External Functions - View & Checks
|
||||
// ====================================================
|
||||
@@ -403,7 +419,7 @@ contract IdentityRegistryImplV1 is IdentityRegistryStorageV1, IIdentityRegistryV
|
||||
* @dev Callable only via a proxy and restricted to the contract owner.
|
||||
* @param newHubAddress The new address of the hub.
|
||||
*/
|
||||
function updateHub(address newHubAddress) external onlyProxy onlyOwner {
|
||||
function updateHub(address newHubAddress) external onlyProxy onlyRole(SECURITY_ROLE) {
|
||||
_hub = newHubAddress;
|
||||
emit HubUpdated(newHubAddress);
|
||||
}
|
||||
@@ -413,7 +429,7 @@ contract IdentityRegistryImplV1 is IdentityRegistryStorageV1, IIdentityRegistryV
|
||||
* @dev Callable only via a proxy and restricted to the contract owner.
|
||||
* @param newPassportNoOfacRoot The new passport number OFAC root value.
|
||||
*/
|
||||
function updatePassportNoOfacRoot(uint256 newPassportNoOfacRoot) external onlyProxy onlyOwner {
|
||||
function updatePassportNoOfacRoot(uint256 newPassportNoOfacRoot) external onlyProxy onlyRole(OPERATIONS_ROLE) {
|
||||
_passportNoOfacRoot = newPassportNoOfacRoot;
|
||||
emit PassportNoOfacRootUpdated(newPassportNoOfacRoot);
|
||||
}
|
||||
@@ -423,7 +439,7 @@ contract IdentityRegistryImplV1 is IdentityRegistryStorageV1, IIdentityRegistryV
|
||||
* @dev Callable only via a proxy and restricted to the contract owner.
|
||||
* @param newNameAndDobOfacRoot The new name and date of birth OFAC root value.
|
||||
*/
|
||||
function updateNameAndDobOfacRoot(uint256 newNameAndDobOfacRoot) external onlyProxy onlyOwner {
|
||||
function updateNameAndDobOfacRoot(uint256 newNameAndDobOfacRoot) external onlyProxy onlyRole(OPERATIONS_ROLE) {
|
||||
_nameAndDobOfacRoot = newNameAndDobOfacRoot;
|
||||
emit NameAndDobOfacRootUpdated(newNameAndDobOfacRoot);
|
||||
}
|
||||
@@ -433,7 +449,7 @@ contract IdentityRegistryImplV1 is IdentityRegistryStorageV1, IIdentityRegistryV
|
||||
* @dev Callable only via a proxy and restricted to the contract owner.
|
||||
* @param newNameAndYobOfacRoot The new name and year of birth OFAC root value.
|
||||
*/
|
||||
function updateNameAndYobOfacRoot(uint256 newNameAndYobOfacRoot) external onlyProxy onlyOwner {
|
||||
function updateNameAndYobOfacRoot(uint256 newNameAndYobOfacRoot) external onlyProxy onlyRole(OPERATIONS_ROLE) {
|
||||
_nameAndYobOfacRoot = newNameAndYobOfacRoot;
|
||||
emit NameAndYobOfacRootUpdated(newNameAndYobOfacRoot);
|
||||
}
|
||||
@@ -443,7 +459,7 @@ contract IdentityRegistryImplV1 is IdentityRegistryStorageV1, IIdentityRegistryV
|
||||
* @dev Callable only via a proxy and restricted to the contract owner.
|
||||
* @param newCscaRoot The new CSCA root value.
|
||||
*/
|
||||
function updateCscaRoot(uint256 newCscaRoot) external onlyProxy onlyOwner {
|
||||
function updateCscaRoot(uint256 newCscaRoot) external onlyProxy onlyRole(OPERATIONS_ROLE) {
|
||||
_cscaRoot = newCscaRoot;
|
||||
emit CscaRootUpdated(newCscaRoot);
|
||||
}
|
||||
@@ -459,7 +475,7 @@ contract IdentityRegistryImplV1 is IdentityRegistryStorageV1, IIdentityRegistryV
|
||||
bytes32 attestationId,
|
||||
uint256 nullifier,
|
||||
uint256 commitment
|
||||
) external onlyProxy onlyOwner {
|
||||
) external onlyProxy onlyRole(SECURITY_ROLE) {
|
||||
_nullifiers[attestationId][nullifier] = true;
|
||||
uint256 imt_root = _addCommitment(_identityCommitmentIMT, commitment);
|
||||
_rootTimestamps[imt_root] = block.timestamp;
|
||||
@@ -478,7 +494,7 @@ contract IdentityRegistryImplV1 is IdentityRegistryStorageV1, IIdentityRegistryV
|
||||
uint256 oldLeaf,
|
||||
uint256 newLeaf,
|
||||
uint256[] calldata siblingNodes
|
||||
) external onlyProxy onlyOwner {
|
||||
) external onlyProxy onlyRole(SECURITY_ROLE) {
|
||||
uint256 imt_root = _updateCommitment(_identityCommitmentIMT, oldLeaf, newLeaf, siblingNodes);
|
||||
_rootTimestamps[imt_root] = block.timestamp;
|
||||
emit DevCommitmentUpdated(oldLeaf, newLeaf, imt_root, block.timestamp);
|
||||
@@ -490,7 +506,10 @@ contract IdentityRegistryImplV1 is IdentityRegistryStorageV1, IIdentityRegistryV
|
||||
* @param oldLeaf The identity commitment to remove.
|
||||
* @param siblingNodes An array of sibling nodes for Merkle proof generation.
|
||||
*/
|
||||
function devRemoveCommitment(uint256 oldLeaf, uint256[] calldata siblingNodes) external onlyProxy onlyOwner {
|
||||
function devRemoveCommitment(
|
||||
uint256 oldLeaf,
|
||||
uint256[] calldata siblingNodes
|
||||
) external onlyProxy onlyRole(SECURITY_ROLE) {
|
||||
uint256 imt_root = _removeCommitment(_identityCommitmentIMT, oldLeaf, siblingNodes);
|
||||
_rootTimestamps[imt_root] = block.timestamp;
|
||||
emit DevCommitmentRemoved(oldLeaf, imt_root, block.timestamp);
|
||||
@@ -501,7 +520,7 @@ contract IdentityRegistryImplV1 is IdentityRegistryStorageV1, IIdentityRegistryV
|
||||
* @dev Callable only by the owner for testing or administration.
|
||||
* @param dscCommitment The DSC key commitment to add.
|
||||
*/
|
||||
function devAddDscKeyCommitment(uint256 dscCommitment) external onlyProxy onlyOwner {
|
||||
function devAddDscKeyCommitment(uint256 dscCommitment) external onlyProxy onlyRole(SECURITY_ROLE) {
|
||||
_isRegisteredDscKeyCommitment[dscCommitment] = true;
|
||||
uint256 imt_root = _addCommitment(_dscKeyCommitmentIMT, dscCommitment);
|
||||
uint256 index = _dscKeyCommitmentIMT._indexOf(dscCommitment);
|
||||
@@ -519,7 +538,7 @@ contract IdentityRegistryImplV1 is IdentityRegistryStorageV1, IIdentityRegistryV
|
||||
uint256 oldLeaf,
|
||||
uint256 newLeaf,
|
||||
uint256[] calldata siblingNodes
|
||||
) external onlyProxy onlyOwner {
|
||||
) external onlyProxy onlyRole(SECURITY_ROLE) {
|
||||
uint256 imt_root = _updateCommitment(_dscKeyCommitmentIMT, oldLeaf, newLeaf, siblingNodes);
|
||||
emit DevDscKeyCommitmentUpdated(oldLeaf, newLeaf, imt_root);
|
||||
}
|
||||
@@ -530,7 +549,10 @@ contract IdentityRegistryImplV1 is IdentityRegistryStorageV1, IIdentityRegistryV
|
||||
* @param oldLeaf The DSC key commitment to remove.
|
||||
* @param siblingNodes An array of sibling nodes for Merkle proof generation.
|
||||
*/
|
||||
function devRemoveDscKeyCommitment(uint256 oldLeaf, uint256[] calldata siblingNodes) external onlyProxy onlyOwner {
|
||||
function devRemoveDscKeyCommitment(
|
||||
uint256 oldLeaf,
|
||||
uint256[] calldata siblingNodes
|
||||
) external onlyProxy onlyRole(SECURITY_ROLE) {
|
||||
uint256 imt_root = _removeCommitment(_dscKeyCommitmentIMT, oldLeaf, siblingNodes);
|
||||
emit DevDscKeyCommitmentRemoved(oldLeaf, imt_root);
|
||||
}
|
||||
@@ -546,7 +568,7 @@ contract IdentityRegistryImplV1 is IdentityRegistryStorageV1, IIdentityRegistryV
|
||||
bytes32 attestationId,
|
||||
uint256 nullifier,
|
||||
bool state
|
||||
) external onlyProxy onlyOwner {
|
||||
) external onlyProxy onlyRole(SECURITY_ROLE) {
|
||||
_nullifiers[attestationId][nullifier] = state;
|
||||
emit DevNullifierStateChanged(attestationId, nullifier, state);
|
||||
}
|
||||
@@ -557,7 +579,10 @@ contract IdentityRegistryImplV1 is IdentityRegistryStorageV1, IIdentityRegistryV
|
||||
* @param dscCommitment The DSC key commitment.
|
||||
* @param state The new state of the DSC key commitment (true for registered, false for not registered).
|
||||
*/
|
||||
function devChangeDscKeyCommitmentState(uint256 dscCommitment, bool state) external onlyProxy onlyOwner {
|
||||
function devChangeDscKeyCommitmentState(
|
||||
uint256 dscCommitment,
|
||||
bool state
|
||||
) external onlyProxy onlyRole(SECURITY_ROLE) {
|
||||
_isRegisteredDscKeyCommitment[dscCommitment] = state;
|
||||
emit DevDscKeyCommitmentStateChanged(dscCommitment, state);
|
||||
}
|
||||
|
||||
@@ -3,22 +3,36 @@ pragma solidity 0.8.28;
|
||||
|
||||
import {IIdentityVerificationHubV1} from "../interfaces/IIdentityVerificationHubV1.sol";
|
||||
import {IIdentityRegistryV1} from "../interfaces/IIdentityRegistryV1.sol";
|
||||
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
|
||||
import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
|
||||
import {CircuitConstants} from "../constants/CircuitConstants.sol";
|
||||
|
||||
/// @title VerifyAll
|
||||
/// @notice A contract for verifying identity proofs and revealing selected data
|
||||
/// @dev This contract interacts with IdentityVerificationHub and IdentityRegistry
|
||||
contract VerifyAll is Ownable {
|
||||
contract VerifyAll is AccessControl {
|
||||
/// @notice Critical operations and role management requiring 3/5 multisig consensus
|
||||
bytes32 public constant SECURITY_ROLE = keccak256("SECURITY_ROLE");
|
||||
|
||||
/// @notice Standard operations requiring 2/5 multisig consensus
|
||||
bytes32 public constant OPERATIONS_ROLE = keccak256("OPERATIONS_ROLE");
|
||||
|
||||
IIdentityVerificationHubV1 public hub;
|
||||
IIdentityRegistryV1 public registry;
|
||||
|
||||
/// @notice Initializes the contract with hub and registry addresses
|
||||
/// @param hubAddress The address of the IdentityVerificationHub contract
|
||||
/// @param registryAddress The address of the IdentityRegistry contract
|
||||
constructor(address hubAddress, address registryAddress) Ownable(msg.sender) {
|
||||
constructor(address hubAddress, address registryAddress) {
|
||||
hub = IIdentityVerificationHubV1(hubAddress);
|
||||
registry = IIdentityRegistryV1(registryAddress);
|
||||
|
||||
// Grant all roles to deployer initially
|
||||
_grantRole(SECURITY_ROLE, msg.sender);
|
||||
_grantRole(OPERATIONS_ROLE, msg.sender);
|
||||
|
||||
// Set role admins - SECURITY_ROLE manages all roles
|
||||
_setRoleAdmin(SECURITY_ROLE, SECURITY_ROLE);
|
||||
_setRoleAdmin(OPERATIONS_ROLE, SECURITY_ROLE);
|
||||
}
|
||||
|
||||
/// @notice Verifies identity proof and reveals selected data
|
||||
@@ -107,15 +121,15 @@ contract VerifyAll is Ownable {
|
||||
|
||||
/// @notice Updates the hub contract address
|
||||
/// @param hubAddress The new hub contract address
|
||||
/// @dev Only callable by the contract owner
|
||||
function setHub(address hubAddress) external onlyOwner {
|
||||
/// @dev Only callable by accounts with SECURITY_ROLE
|
||||
function setHub(address hubAddress) external onlyRole(SECURITY_ROLE) {
|
||||
hub = IIdentityVerificationHubV1(hubAddress);
|
||||
}
|
||||
|
||||
/// @notice Updates the registry contract address
|
||||
/// @param registryAddress The new registry contract address
|
||||
/// @dev Only callable by the contract owner
|
||||
function setRegistry(address registryAddress) external onlyOwner {
|
||||
/// @dev Only callable by accounts with SECURITY_ROLE
|
||||
function setRegistry(address registryAddress) external onlyRole(SECURITY_ROLE) {
|
||||
registry = IIdentityRegistryV1(registryAddress);
|
||||
}
|
||||
}
|
||||
|
||||
70
contracts/contracts/tests/MockOwnableHub.sol
Normal file
70
contracts/contracts/tests/MockOwnableHub.sol
Normal file
@@ -0,0 +1,70 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity 0.8.28;
|
||||
|
||||
import {MockOwnableImplRoot} from "./MockOwnableImplRoot.sol";
|
||||
|
||||
/**
|
||||
* @title MockOwnableHub
|
||||
* @dev Mock contract that simulates the OLD production Hub using Ownable
|
||||
* This represents what's currently deployed in production before the governance upgrade.
|
||||
*/
|
||||
contract MockOwnableHub is MockOwnableImplRoot {
|
||||
/// @notice Circuit version for compatibility
|
||||
uint256 private _circuitVersion;
|
||||
|
||||
/// @notice Registry address
|
||||
address private _registry;
|
||||
|
||||
/// @notice Event emitted when hub is initialized
|
||||
event HubInitialized();
|
||||
|
||||
/// @notice Event emitted when registry is updated
|
||||
event RegistryUpdated(address indexed registry);
|
||||
|
||||
/**
|
||||
* @notice Constructor that disables initializers for the implementation contract.
|
||||
*/
|
||||
constructor() {
|
||||
_disableInitializers();
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Initializes the Hub contract (simulates production initialization)
|
||||
*/
|
||||
function initialize() external initializer {
|
||||
__MockOwnableImplRoot_init();
|
||||
_circuitVersion = 1;
|
||||
emit HubInitialized();
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Updates the registry address (simulates production function)
|
||||
* @param registryAddress The new registry address
|
||||
*/
|
||||
function updateRegistry(address registryAddress) external onlyOwner {
|
||||
_registry = registryAddress;
|
||||
emit RegistryUpdated(registryAddress);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Gets the circuit version
|
||||
*/
|
||||
function getCircuitVersion() external view returns (uint256) {
|
||||
return _circuitVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Gets the registry address
|
||||
*/
|
||||
function getRegistry() external view returns (address) {
|
||||
return _registry;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Updates the circuit version
|
||||
* @param version The new circuit version
|
||||
*/
|
||||
function updateCircuitVersion(uint256 version) external onlyOwner {
|
||||
_circuitVersion = version;
|
||||
}
|
||||
}
|
||||
31
contracts/contracts/tests/MockOwnableImplRoot.sol
Normal file
31
contracts/contracts/tests/MockOwnableImplRoot.sol
Normal file
@@ -0,0 +1,31 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity 0.8.28;
|
||||
|
||||
import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
|
||||
import {Ownable2StepUpgradeable} from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol";
|
||||
|
||||
/**
|
||||
* @title MockOwnableImplRoot
|
||||
* @dev Mock contract that simulates the OLD production ImplRoot using Ownable2StepUpgradeable
|
||||
* This represents what's currently deployed in production before the governance upgrade.
|
||||
*/
|
||||
abstract contract MockOwnableImplRoot is UUPSUpgradeable, Ownable2StepUpgradeable {
|
||||
// Reserved storage space to allow for layout changes in the future.
|
||||
uint256[50] private __gap;
|
||||
|
||||
/**
|
||||
* @dev Initializes the contract by setting the deployer as the initial owner and initializing
|
||||
* the UUPS proxy functionality.
|
||||
*/
|
||||
function __MockOwnableImplRoot_init() internal virtual onlyInitializing {
|
||||
__Ownable_init(msg.sender);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Authorizes an upgrade to a new implementation.
|
||||
* Requirements:
|
||||
* - Must be called through a proxy.
|
||||
* - Caller must be the owner.
|
||||
*/
|
||||
function _authorizeUpgrade(address newImplementation) internal virtual override onlyProxy onlyOwner {}
|
||||
}
|
||||
102
contracts/contracts/tests/MockOwnableRegistry.sol
Normal file
102
contracts/contracts/tests/MockOwnableRegistry.sol
Normal file
@@ -0,0 +1,102 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity 0.8.28;
|
||||
|
||||
import {MockOwnableImplRoot} from "./MockOwnableImplRoot.sol";
|
||||
|
||||
/**
|
||||
* @title MockOwnableRegistry
|
||||
* @dev Mock contract that simulates the OLD production Registry using Ownable
|
||||
* This represents what's currently deployed in production before the governance upgrade.
|
||||
*/
|
||||
contract MockOwnableRegistry is MockOwnableImplRoot {
|
||||
/// @notice Hub address
|
||||
address private _hub;
|
||||
|
||||
/// @notice CSCA Root
|
||||
bytes32 private _cscaRoot;
|
||||
|
||||
/// @notice Some registry data
|
||||
mapping(bytes32 => bool) private _commitments;
|
||||
|
||||
/// @notice Event emitted when registry is initialized
|
||||
event RegistryInitialized(address indexed hub);
|
||||
|
||||
/// @notice Event emitted when hub is updated
|
||||
event HubUpdated(address indexed hub);
|
||||
|
||||
/// @notice Event emitted when CSCA root is updated
|
||||
event CscaRootUpdated(bytes32 indexed cscaRoot);
|
||||
|
||||
/**
|
||||
* @notice Constructor that disables initializers for the implementation contract.
|
||||
*/
|
||||
constructor() {
|
||||
_disableInitializers();
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Initializes the Registry contract (simulates production initialization)
|
||||
* @param hubAddress The hub address
|
||||
*/
|
||||
function initialize(address hubAddress) external initializer {
|
||||
__MockOwnableImplRoot_init();
|
||||
_hub = hubAddress;
|
||||
emit RegistryInitialized(hubAddress);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Sets the hub address (simulates production function)
|
||||
* @param hubAddress The new hub address
|
||||
*/
|
||||
function setHub(address hubAddress) external onlyOwner {
|
||||
_hub = hubAddress;
|
||||
emit HubUpdated(hubAddress);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Updates the hub address (simulates production function)
|
||||
* @param hubAddress The new hub address
|
||||
*/
|
||||
function updateHub(address hubAddress) external onlyOwner {
|
||||
_hub = hubAddress;
|
||||
emit HubUpdated(hubAddress);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Updates the CSCA root (simulates production function)
|
||||
* @param cscaRoot The new CSCA root
|
||||
*/
|
||||
function updateCscaRoot(bytes32 cscaRoot) external onlyOwner {
|
||||
_cscaRoot = cscaRoot;
|
||||
emit CscaRootUpdated(cscaRoot);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Adds a commitment (simulates production function)
|
||||
* @param commitment The commitment to add
|
||||
*/
|
||||
function addCommitment(bytes32 commitment) external onlyOwner {
|
||||
_commitments[commitment] = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Gets the hub address
|
||||
*/
|
||||
function getHub() external view returns (address) {
|
||||
return _hub;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Gets the CSCA root
|
||||
*/
|
||||
function getCscaRoot() external view returns (bytes32) {
|
||||
return _cscaRoot;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Checks if a commitment exists
|
||||
*/
|
||||
function hasCommitment(bytes32 commitment) external view returns (bool) {
|
||||
return _commitments[commitment];
|
||||
}
|
||||
}
|
||||
82
contracts/contracts/tests/MockUpgradedHub.sol
Normal file
82
contracts/contracts/tests/MockUpgradedHub.sol
Normal file
@@ -0,0 +1,82 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity 0.8.28;
|
||||
|
||||
import {ImplRoot} from "../upgradeable/ImplRoot.sol";
|
||||
|
||||
/**
|
||||
* @title MockUpgradedHub
|
||||
* @dev Mock contract that simulates the NEW Hub with AccessControl governance
|
||||
* This represents what the Hub will look like after the governance upgrade.
|
||||
*/
|
||||
contract MockUpgradedHub is ImplRoot {
|
||||
/// @notice Circuit version for compatibility
|
||||
uint256 private _circuitVersion;
|
||||
|
||||
/// @notice Registry address
|
||||
address private _registry;
|
||||
|
||||
/// @notice Event emitted when hub is initialized with governance
|
||||
event HubGovernanceInitialized();
|
||||
|
||||
/// @notice Event emitted when registry is updated
|
||||
event RegistryUpdated(address indexed registry);
|
||||
|
||||
/**
|
||||
* @notice Constructor that disables initializers for the implementation contract.
|
||||
*/
|
||||
constructor() {
|
||||
_disableInitializers();
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Initializes governance for the upgraded Hub
|
||||
* This should be called after the upgrade to set up AccessControl
|
||||
* NOTE: This ONLY initializes governance roles, does NOT modify existing state
|
||||
*/
|
||||
function initialize() external reinitializer(2) {
|
||||
__ImplRoot_init();
|
||||
// DO NOT modify _registry or _circuitVersion - they should be preserved from before upgrade!
|
||||
emit HubGovernanceInitialized();
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Updates the registry address (now requires SECURITY_ROLE)
|
||||
* @param registryAddress The new registry address
|
||||
*/
|
||||
function updateRegistry(address registryAddress) external onlyRole(SECURITY_ROLE) {
|
||||
_registry = registryAddress;
|
||||
emit RegistryUpdated(registryAddress);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Updates circuit version (requires SECURITY_ROLE)
|
||||
* @param version The new circuit version
|
||||
*/
|
||||
function updateCircuitVersion(uint256 version) external onlyRole(SECURITY_ROLE) {
|
||||
_circuitVersion = version;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Gets the circuit version
|
||||
*/
|
||||
function getCircuitVersion() external view returns (uint256) {
|
||||
return _circuitVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Gets the registry address
|
||||
*/
|
||||
function getRegistry() external view returns (address) {
|
||||
return _registry;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Checks if the upgrade preserved critical storage data
|
||||
* This is a verification function to ensure storage migration worked
|
||||
*/
|
||||
function verifyStorageMigration() external view returns (bool) {
|
||||
// The important thing is that the contract is functional and governance works
|
||||
// Registry and circuit version should be preserved, deprecated owner may be zero
|
||||
return hasRole(SECURITY_ROLE, msg.sender) || hasRole(OPERATIONS_ROLE, msg.sender);
|
||||
}
|
||||
}
|
||||
103
contracts/contracts/tests/MockUpgradedRegistry.sol
Normal file
103
contracts/contracts/tests/MockUpgradedRegistry.sol
Normal file
@@ -0,0 +1,103 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity 0.8.28;
|
||||
|
||||
import {ImplRoot} from "../upgradeable/ImplRoot.sol";
|
||||
|
||||
/**
|
||||
* @title MockUpgradedRegistry
|
||||
* @dev Mock contract that simulates the NEW Registry with AccessControl governance
|
||||
* This represents what the Registry will look like after the governance upgrade.
|
||||
*/
|
||||
contract MockUpgradedRegistry is ImplRoot {
|
||||
/// @notice Hub address
|
||||
address private _hub;
|
||||
|
||||
/// @notice CSCA Root
|
||||
bytes32 private _cscaRoot;
|
||||
|
||||
/// @notice Some registry data
|
||||
mapping(bytes32 => bool) private _commitments;
|
||||
|
||||
/// @notice Event emitted when registry governance is initialized
|
||||
event RegistryGovernanceInitialized();
|
||||
|
||||
/// @notice Event emitted when hub is updated
|
||||
event HubUpdated(address indexed hub);
|
||||
|
||||
/// @notice Event emitted when CSCA root is updated
|
||||
event CscaRootUpdated(bytes32 indexed cscaRoot);
|
||||
|
||||
/**
|
||||
* @notice Constructor that disables initializers for the implementation contract.
|
||||
*/
|
||||
constructor() {
|
||||
_disableInitializers();
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Initializes governance for the upgraded Registry
|
||||
* This should be called after the upgrade to set up AccessControl
|
||||
* NOTE: This ONLY initializes governance roles, does NOT modify existing state
|
||||
*/
|
||||
function initialize() external reinitializer(2) {
|
||||
__ImplRoot_init();
|
||||
// DO NOT modify _hub or _cscaRoot - they should be preserved from before upgrade!
|
||||
emit RegistryGovernanceInitialized();
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Sets the hub address (now requires SECURITY_ROLE)
|
||||
* @param hubAddress The new hub address
|
||||
*/
|
||||
function setHub(address hubAddress) external onlyRole(SECURITY_ROLE) {
|
||||
_hub = hubAddress;
|
||||
emit HubUpdated(hubAddress);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Updates the hub address (now requires SECURITY_ROLE)
|
||||
* @param hubAddress The new hub address
|
||||
*/
|
||||
function updateHub(address hubAddress) external onlyRole(SECURITY_ROLE) {
|
||||
_hub = hubAddress;
|
||||
emit HubUpdated(hubAddress);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Updates the CSCA root (now requires SECURITY_ROLE)
|
||||
* @param cscaRoot The new CSCA root
|
||||
*/
|
||||
function updateCscaRoot(bytes32 cscaRoot) external onlyRole(OPERATIONS_ROLE) {
|
||||
_cscaRoot = cscaRoot;
|
||||
emit CscaRootUpdated(cscaRoot);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Adds a commitment (now requires SECURITY_ROLE)
|
||||
* @param commitment The commitment to add
|
||||
*/
|
||||
function addCommitment(bytes32 commitment) external onlyRole(SECURITY_ROLE) {
|
||||
_commitments[commitment] = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Gets the hub address
|
||||
*/
|
||||
function getHub() external view returns (address) {
|
||||
return _hub;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Gets the CSCA root
|
||||
*/
|
||||
function getCscaRoot() external view returns (bytes32) {
|
||||
return _cscaRoot;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Checks if a commitment exists
|
||||
*/
|
||||
function hasCommitment(bytes32 commitment) external view returns (bool) {
|
||||
return _commitments[commitment];
|
||||
}
|
||||
}
|
||||
@@ -4,15 +4,23 @@ pragma solidity 0.8.28;
|
||||
import {ImplRoot} from "../../contracts/upgradeable/ImplRoot.sol";
|
||||
|
||||
contract MockImplRoot is ImplRoot {
|
||||
function exposed__ImplRoot_init() external {
|
||||
function exposed__ImplRoot_init() external initializer {
|
||||
__ImplRoot_init();
|
||||
}
|
||||
|
||||
function exposed__Ownable_init(address initialOwner) external initializer {
|
||||
__Ownable_init(initialOwner);
|
||||
}
|
||||
|
||||
function exposed_authorizeUpgrade(address newImplementation) external {
|
||||
_authorizeUpgrade(newImplementation);
|
||||
}
|
||||
|
||||
function exposed_grantRole(bytes32 role, address account) external {
|
||||
_grantRole(role, account);
|
||||
}
|
||||
|
||||
function exposed_revokeRole(bytes32 role, address account) external {
|
||||
_revokeRole(role, account);
|
||||
}
|
||||
|
||||
function exposed_setRoleAdmin(bytes32 role, bytes32 adminRole) external {
|
||||
_setRoleAdmin(role, adminRole);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ contract testUpgradedIdentityVerificationHubImplV1 is
|
||||
* @param isTestInput Boolean value which shows it is test or not
|
||||
*/
|
||||
function initialize(bool isTestInput) external reinitializer(3) {
|
||||
__ImplRoot_init();
|
||||
__Ownable_init(msg.sender);
|
||||
_isTest = isTestInput;
|
||||
emit TestHubInitialized();
|
||||
}
|
||||
|
||||
@@ -2,15 +2,25 @@
|
||||
pragma solidity 0.8.28;
|
||||
|
||||
import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
|
||||
import {Ownable2StepUpgradeable} from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol";
|
||||
import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
|
||||
|
||||
/**
|
||||
* @title ImplRoot
|
||||
* @dev Abstract contract providing upgradeable functionality via UUPSUpgradeable,
|
||||
* along with a two-step ownable mechanism using Ownable2StepUpgradeable.
|
||||
* along with role-based access control using AccessControlUpgradeable.
|
||||
* Serves as a base for upgradeable implementations.
|
||||
*
|
||||
* Governance Roles:
|
||||
* - SECURITY_ROLE: Security-sensitive operations and role management (3/5 multisig consensus)
|
||||
* - OPERATIONS_ROLE: Routine operational tasks (2/5 multisig consensus)
|
||||
*/
|
||||
abstract contract ImplRoot is UUPSUpgradeable, Ownable2StepUpgradeable {
|
||||
abstract contract ImplRoot is UUPSUpgradeable, AccessControlUpgradeable {
|
||||
/// @notice Security-sensitive operations requiring 3/5 multisig consensus
|
||||
bytes32 public constant SECURITY_ROLE = keccak256("SECURITY_ROLE");
|
||||
|
||||
/// @notice Routine operations requiring 2/5 multisig consensus
|
||||
bytes32 public constant OPERATIONS_ROLE = keccak256("OPERATIONS_ROLE");
|
||||
|
||||
// Reserved storage space to allow for layout changes in the future.
|
||||
uint256[50] private __gap;
|
||||
|
||||
@@ -21,17 +31,23 @@ abstract contract ImplRoot is UUPSUpgradeable, Ownable2StepUpgradeable {
|
||||
* This function should be called in the initializer of the derived contract.
|
||||
*/
|
||||
function __ImplRoot_init() internal virtual onlyInitializing {
|
||||
__Ownable_init(msg.sender);
|
||||
__UUPSUpgradeable_init();
|
||||
__AccessControl_init();
|
||||
|
||||
_grantRole(SECURITY_ROLE, msg.sender);
|
||||
_grantRole(OPERATIONS_ROLE, msg.sender);
|
||||
|
||||
// Set role admins - SECURITY_ROLE manages all roles
|
||||
_setRoleAdmin(SECURITY_ROLE, SECURITY_ROLE);
|
||||
_setRoleAdmin(OPERATIONS_ROLE, SECURITY_ROLE);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Authorizes an upgrade to a new implementation.
|
||||
* Requirements:
|
||||
* - Must be called through a proxy.
|
||||
* - Caller must be the contract owner.
|
||||
* - Caller must have SECURITY_ROLE.
|
||||
*
|
||||
* @param newImplementation The address of the new implementation contract.
|
||||
*/
|
||||
function _authorizeUpgrade(address newImplementation) internal virtual override onlyProxy onlyOwner {}
|
||||
function _authorizeUpgrade(address newImplementation) internal virtual override onlyProxy onlyRole(SECURITY_ROLE) {}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,32 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity 0.8.28;
|
||||
|
||||
import "@openzeppelin/contracts/access/Ownable.sol";
|
||||
import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
|
||||
|
||||
/**
|
||||
* @title PCR0Manager
|
||||
* @notice This contract manages a mapping of PCR0 values (provided as a 48-byte value)
|
||||
* to booleans. The PCR0 value (the 48-byte SHA384 output) is hashed
|
||||
* using keccak256 and then stored in the mapping.
|
||||
* Only the owner can add or remove entries.
|
||||
* Only accounts with SECURITY_ROLE can add or remove entries.
|
||||
* @custom:version 1.2.0
|
||||
*/
|
||||
contract PCR0Manager is Ownable {
|
||||
// Pass msg.sender directly to Ownable constructor
|
||||
constructor() Ownable(msg.sender) {}
|
||||
contract PCR0Manager is AccessControl {
|
||||
/// @notice Critical operations and role management requiring 3/5 multisig consensus
|
||||
bytes32 public constant SECURITY_ROLE = keccak256("SECURITY_ROLE");
|
||||
|
||||
/// @notice Standard operations requiring 2/5 multisig consensus
|
||||
bytes32 public constant OPERATIONS_ROLE = keccak256("OPERATIONS_ROLE");
|
||||
|
||||
constructor() {
|
||||
// Grant all roles to deployer initially
|
||||
_grantRole(SECURITY_ROLE, msg.sender);
|
||||
_grantRole(OPERATIONS_ROLE, msg.sender);
|
||||
|
||||
// Set role admins - SECURITY_ROLE is admin of both roles
|
||||
_setRoleAdmin(SECURITY_ROLE, SECURITY_ROLE);
|
||||
_setRoleAdmin(OPERATIONS_ROLE, SECURITY_ROLE);
|
||||
}
|
||||
|
||||
// Mapping from keccak256(pcr0) to its boolean state.
|
||||
mapping(bytes32 => bool) public pcr0Mapping;
|
||||
@@ -27,12 +41,14 @@ contract PCR0Manager is Ownable {
|
||||
|
||||
/**
|
||||
* @notice Adds a new PCR0 entry by setting its value to true.
|
||||
* @param pcr0 The PCR0 value (must be exactly 48 bytes).
|
||||
* @dev Reverts if the PCR0 value is not 48 bytes or if it is already set.
|
||||
* @param pcr0 The PCR0 value (must be exactly 32 bytes).
|
||||
* @dev Reverts if the PCR0 value is not 32 bytes or if it is already set.
|
||||
* @dev Pads the PCR0 value to 48 bytes by prefixing 16 zero bytes to maintain mobile app compatibility.
|
||||
*/
|
||||
function addPCR0(bytes calldata pcr0) external onlyOwner {
|
||||
require(pcr0.length == 48, "PCR0 must be 48 bytes");
|
||||
bytes32 key = keccak256(pcr0);
|
||||
function addPCR0(bytes calldata pcr0) external onlyRole(SECURITY_ROLE) {
|
||||
require(pcr0.length == 32, "PCR0 must be 32 bytes");
|
||||
bytes memory paddedPcr0 = abi.encodePacked(new bytes(16), pcr0);
|
||||
bytes32 key = keccak256(paddedPcr0);
|
||||
require(!pcr0Mapping[key], "PCR0 already set");
|
||||
pcr0Mapping[key] = true;
|
||||
emit PCR0Added(key);
|
||||
@@ -40,12 +56,14 @@ contract PCR0Manager is Ownable {
|
||||
|
||||
/**
|
||||
* @notice Removes an existing PCR0 entry by setting its value to false.
|
||||
* @param pcr0 The PCR0 value (must be exactly 48 bytes).
|
||||
* @dev Reverts if the PCR0 value is not 48 bytes or if it is not currently set.
|
||||
* @param pcr0 The PCR0 value (must be exactly 32 bytes).
|
||||
* @dev Reverts if the PCR0 value is not 32 bytes or if it is not currently set.
|
||||
* @dev Pads the PCR0 value to 48 bytes by prefixing 16 zero bytes to maintain mobile app compatibility.
|
||||
*/
|
||||
function removePCR0(bytes calldata pcr0) external onlyOwner {
|
||||
require(pcr0.length == 48, "PCR0 must be 48 bytes");
|
||||
bytes32 key = keccak256(pcr0);
|
||||
function removePCR0(bytes calldata pcr0) external onlyRole(SECURITY_ROLE) {
|
||||
require(pcr0.length == 32, "PCR0 must be 32 bytes");
|
||||
bytes memory paddedPcr0 = abi.encodePacked(new bytes(16), pcr0);
|
||||
bytes32 key = keccak256(paddedPcr0);
|
||||
require(pcr0Mapping[key], "PCR0 not set");
|
||||
pcr0Mapping[key] = false;
|
||||
emit PCR0Removed(key);
|
||||
@@ -54,6 +72,8 @@ contract PCR0Manager is Ownable {
|
||||
/**
|
||||
* @notice Checks whether a given PCR0 value is set to true in the mapping.
|
||||
* @param pcr0 The PCR0 value (must be exactly 48 bytes).
|
||||
* @dev Does not pad the PCR0 value as this is handled by the mobile app.
|
||||
* @dev If you are manually calling this function, you need to pad the PCR0 value to 48 bytes, prefixing 16 zero bytes.
|
||||
* @return exists True if the PCR0 entry is set, false otherwise.
|
||||
*/
|
||||
function isPCR0Set(bytes calldata pcr0) external view returns (bool exists) {
|
||||
|
||||
@@ -37,25 +37,25 @@ contract Verifier_register_aadhaar {
|
||||
uint256 constant gammax2 = 10857046999023057135944570762232829481370756359578518086990519993285655852781;
|
||||
uint256 constant gammay1 = 4082367875863433681332203403145435568316851327593401208105741076214120093531;
|
||||
uint256 constant gammay2 = 8495653923123431417604973247489272438418190587263600148770280649306958101930;
|
||||
uint256 constant deltax1 = 1184175006002790631176821634090938467107330227007158853824891629496015889924;
|
||||
uint256 constant deltax2 = 12086636205582787465813058141825079064824697543086779109775595053805081617827;
|
||||
uint256 constant deltay1 = 4456837667197728326322115376478122146150647259307011732553476664405503785753;
|
||||
uint256 constant deltay2 = 9088696651190771223855139438876954166862164661620992858425695135876196457926;
|
||||
uint256 constant deltax1 = 3953219198104570901098823830840773856017689139278458081183220490752145815050;
|
||||
uint256 constant deltax2 = 428186582661072144108009098107578252463491462238432931497262180014713596115;
|
||||
uint256 constant deltay1 = 18162968189172780580333095558539690618880186036957545736311404283407493778880;
|
||||
uint256 constant deltay2 = 7343682947937413219111184190299798376421430633278379635221606345245958931239;
|
||||
|
||||
uint256 constant IC0x = 6547380589242664979389953612506618657067204598675122139604885565320676833158;
|
||||
uint256 constant IC0y = 19055399919951028177234969337049077818155869440497248883170998389487338107126;
|
||||
uint256 constant IC0x = 18984838814932147425072354846429508676387686524229308734161716095463360490134;
|
||||
uint256 constant IC0y = 11220857659665071811279473081460089783437970319349511357818317231596300603739;
|
||||
|
||||
uint256 constant IC1x = 20557545828033851521979343305884318041481443328161582179150888164584749744669;
|
||||
uint256 constant IC1y = 21560118189953885636148717201222479281100786469743463492679572665614931385205;
|
||||
|
||||
uint256 constant IC2x = 17559551632997878871440402139938294429514970824368869332125462241643052815376;
|
||||
uint256 constant IC2y = 18428425902276807983388946110037886804676016275275246286544615654725514849838;
|
||||
uint256 constant IC2x = 16679737504993527028036863898232919844061144900682159566005073982271081014169;
|
||||
uint256 constant IC2y = 608284743266912406546568650108359232826801114144551290914053108404596136834;
|
||||
|
||||
uint256 constant IC3x = 18768989044514693938417600792629717603460465495191187242290958821278680606604;
|
||||
uint256 constant IC3y = 6584358559179261704032830455997936799129839324733806160004605275139747821694;
|
||||
uint256 constant IC3x = 10860675204793746311158823740347274485680296774011483979610282012223174969615;
|
||||
uint256 constant IC3y = 15029247271645078761075880233744321449871203863194823447037797284218806524473;
|
||||
|
||||
uint256 constant IC4x = 16692378542219000347024593964346873649905710163948976095790586330709671710647;
|
||||
uint256 constant IC4y = 2622311591517607336391164955074698243697841582935873217110527812716210930596;
|
||||
uint256 constant IC4x = 17894574390662839711557891944994831304061930223868407277717041388786423798517;
|
||||
uint256 constant IC4y = 10747262533817845366080322542335489573224940900925614580771284497946454436591;
|
||||
|
||||
// Memory data
|
||||
uint16 constant pVk = 0;
|
||||
|
||||
225
contracts/deployments/registry.json
Normal file
225
contracts/deployments/registry.json
Normal file
@@ -0,0 +1,225 @@
|
||||
{
|
||||
"$schema": "./registry.schema.json",
|
||||
"lastUpdated": "2025-12-10T06:17:50.863Z",
|
||||
"contracts": {
|
||||
"IdentityVerificationHub": {
|
||||
"source": "IdentityVerificationHubImplV2",
|
||||
"type": "uups-proxy",
|
||||
"description": "Main identity verification hub for all document types"
|
||||
},
|
||||
"IdentityRegistry": {
|
||||
"source": "IdentityRegistryImplV1",
|
||||
"type": "uups-proxy",
|
||||
"description": "Passport identity registry"
|
||||
},
|
||||
"IdentityRegistryIdCard": {
|
||||
"source": "IdentityRegistryIdCardImplV1",
|
||||
"type": "uups-proxy",
|
||||
"description": "EU ID Card identity registry"
|
||||
},
|
||||
"IdentityRegistryAadhaar": {
|
||||
"source": "IdentityRegistryAadhaarImplV1",
|
||||
"type": "uups-proxy",
|
||||
"description": "Aadhaar identity registry"
|
||||
},
|
||||
"PCR0Manager": {
|
||||
"source": "PCR0Manager",
|
||||
"type": "non-upgradeable",
|
||||
"description": "PCR0 value management for TEE verification"
|
||||
},
|
||||
"VerifyAll": {
|
||||
"source": "VerifyAll",
|
||||
"type": "non-upgradeable",
|
||||
"description": "SDK verification helper contract"
|
||||
}
|
||||
},
|
||||
"networks": {
|
||||
"celo": {
|
||||
"chainId": 42220,
|
||||
"governance": {
|
||||
"securityMultisig": "0x738f0bb37FD3b6C4Cdf8eb6FcdFaAA0CA208CB4A",
|
||||
"operationsMultisig": "0x067b18e09A10Fa03d027c1D60A098CEbbE5637f0",
|
||||
"securityThreshold": "3/5",
|
||||
"operationsThreshold": "2/5"
|
||||
},
|
||||
"deployments": {
|
||||
"IdentityVerificationHub": {
|
||||
"proxy": "0xe57F4773bd9c9d8b6Cd70431117d353298B9f5BF",
|
||||
"currentVersion": "2.12.0",
|
||||
"currentImpl": "0x05FB9D7830889cc389E88198f6A224eA87F01151"
|
||||
},
|
||||
"IdentityRegistry": {
|
||||
"proxy": "0x37F5CB8cB1f6B00aa768D8aA99F1A9289802A968",
|
||||
"currentVersion": "1.2.0",
|
||||
"currentImpl": "0x81E7F74560FAF7eE8DE3a36A5a68B6cbc429Cd36"
|
||||
},
|
||||
"IdentityRegistryIdCard": {
|
||||
"proxy": "0xeAD1E6Ec29c1f3D33a0662f253a3a94D189566E1",
|
||||
"currentVersion": "1.2.0",
|
||||
"currentImpl": "0x7d5e4b7D4c3029aF134D50642674Af8F875118a4"
|
||||
},
|
||||
"IdentityRegistryAadhaar": {
|
||||
"proxy": "0xd603Fa8C8f4694E8DD1DcE1f27C0C3fc91e32Ac4",
|
||||
"currentVersion": "1.2.0",
|
||||
"currentImpl": "0xbD861A9cecf7B0A9631029d55A8CE1155e50697c"
|
||||
},
|
||||
"PCR0Manager": {
|
||||
"address": "0x9743fe2C1c3D2b068c56dE314e9B10DA9c904717",
|
||||
"currentVersion": "1.2.0"
|
||||
},
|
||||
"VerifyAll": {
|
||||
"address": "",
|
||||
"currentVersion": "1.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"localhost": {
|
||||
"chainId": 31337,
|
||||
"governance": {
|
||||
"securityMultisig": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
|
||||
"operationsMultisig": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
|
||||
"securityThreshold": "1/1",
|
||||
"operationsThreshold": "1/1"
|
||||
},
|
||||
"deployments": {}
|
||||
}
|
||||
},
|
||||
"versions": {
|
||||
"IdentityVerificationHub": {
|
||||
"2.12.0": {
|
||||
"initializerVersion": 12,
|
||||
"initializerFunction": "initializeGovernance",
|
||||
"changelog": "Governance upgrade - migrated to AccessControlUpgradeable with multi-tier governance",
|
||||
"gitTag": "hub-v2.12.0",
|
||||
"deployments": {
|
||||
"celo": {
|
||||
"impl": "0x05FB9D7830889cc389E88198f6A224eA87F01151",
|
||||
"deployedAt": "2025-12-10T05:43:58.258Z",
|
||||
"deployedBy": "0xCaEe7aAF115F04D836E2D362A7c07F04db436bd0",
|
||||
"gitCommit": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"2.11.0": {
|
||||
"initializerVersion": 11,
|
||||
"initializerFunction": "initialize",
|
||||
"changelog": "V2 hub deployment with Ownable2StepUpgradeable governance",
|
||||
"gitTag": "hub-v2.11.0",
|
||||
"deployments": {
|
||||
"celo": {
|
||||
"impl": "",
|
||||
"deployedAt": "",
|
||||
"deployedBy": "",
|
||||
"gitCommit": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"IdentityRegistry": {
|
||||
"1.2.0": {
|
||||
"initializerVersion": 2,
|
||||
"initializerFunction": "initializeGovernance",
|
||||
"changelog": "Governance upgrade - migrated to AccessControlUpgradeable",
|
||||
"gitTag": "registry-passport-v1.2.0",
|
||||
"deployments": {
|
||||
"celo": {
|
||||
"impl": "0x81E7F74560FAF7eE8DE3a36A5a68B6cbc429Cd36",
|
||||
"deployedAt": "2025-12-10T05:53:12.534Z",
|
||||
"deployedBy": "0xCaEe7aAF115F04D836E2D362A7c07F04db436bd0",
|
||||
"gitCommit": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"1.1.0": {
|
||||
"initializerVersion": 1,
|
||||
"initializerFunction": "initialize",
|
||||
"changelog": "Initial deployment with Ownable2StepUpgradeable governance",
|
||||
"gitTag": "registry-passport-v1.1.0",
|
||||
"deployments": {
|
||||
"celo": {
|
||||
"impl": "",
|
||||
"deployedAt": "",
|
||||
"deployedBy": "",
|
||||
"gitCommit": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"IdentityRegistryIdCard": {
|
||||
"1.2.0": {
|
||||
"initializerVersion": 2,
|
||||
"initializerFunction": "initializeGovernance",
|
||||
"changelog": "Governance upgrade - migrated to AccessControlUpgradeable",
|
||||
"gitTag": "registry-idcard-v1.2.0",
|
||||
"deployments": {
|
||||
"celo": {
|
||||
"impl": "0x7d5e4b7D4c3029aF134D50642674Af8F875118a4",
|
||||
"deployedAt": "2025-12-10T05:45:56.772Z",
|
||||
"deployedBy": "0xCaEe7aAF115F04D836E2D362A7c07F04db436bd0",
|
||||
"gitCommit": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"1.1.0": {
|
||||
"initializerVersion": 1,
|
||||
"initializerFunction": "initialize",
|
||||
"changelog": "Initial deployment",
|
||||
"gitTag": "registry-idcard-v1.1.0",
|
||||
"deployments": {
|
||||
"celo": {
|
||||
"impl": "",
|
||||
"deployedAt": "",
|
||||
"deployedBy": "",
|
||||
"gitCommit": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"IdentityRegistryAadhaar": {
|
||||
"1.2.0": {
|
||||
"initializerVersion": 2,
|
||||
"initializerFunction": "initializeGovernance",
|
||||
"changelog": "Governance upgrade - migrated to AccessControlUpgradeable",
|
||||
"gitTag": "registry-aadhaar-v1.2.0",
|
||||
"deployments": {
|
||||
"celo": {
|
||||
"impl": "0xbD861A9cecf7B0A9631029d55A8CE1155e50697c",
|
||||
"deployedAt": "2025-12-10T05:47:22.844Z",
|
||||
"deployedBy": "0xCaEe7aAF115F04D836E2D362A7c07F04db436bd0",
|
||||
"gitCommit": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"1.1.0": {
|
||||
"initializerVersion": 1,
|
||||
"initializerFunction": "initialize",
|
||||
"changelog": "Initial deployment",
|
||||
"gitTag": "registry-aadhaar-v1.1.0",
|
||||
"deployments": {
|
||||
"celo": {
|
||||
"impl": "",
|
||||
"deployedAt": "",
|
||||
"deployedBy": "",
|
||||
"gitCommit": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"PCR0Manager": {
|
||||
"1.2.0": {
|
||||
"initializerVersion": 0,
|
||||
"initializerFunction": "",
|
||||
"changelog": "Multisig governance deployment - migrated from single owner to AccessControl",
|
||||
"gitTag": "pcr0manager-v1.2.0",
|
||||
"deployments": {
|
||||
"celo": {
|
||||
"impl": "0x9743fe2C1c3D2b068c56dE314e9B10DA9c904717",
|
||||
"deployedAt": "2025-12-10T06:17:50.863Z",
|
||||
"deployedBy": "0xCaEe7aAF115F04D836E2D362A7c07F04db436bd0",
|
||||
"gitCommit": "5787cff3bcbea870b50eccd7164fbd45b758568e"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
54
contracts/foundry.toml
Normal file
54
contracts/foundry.toml
Normal file
@@ -0,0 +1,54 @@
|
||||
# Foundry Configuration for Hardhat Compatibility
|
||||
# Based on: https://getfoundry.sh/config/hardhat/
|
||||
|
||||
[profile.default]
|
||||
# Use Hardhat's directory structure
|
||||
src = "contracts"
|
||||
out = "out" # Keep separate from Hardhat's artifacts to avoid conflicts
|
||||
libs = ["node_modules", "lib"]
|
||||
test = "test/foundry"
|
||||
script = "script"
|
||||
cache_path = "cache_forge"
|
||||
|
||||
# Enable FFI for OpenZeppelin Upgrades plugin
|
||||
ffi = true
|
||||
ast = true
|
||||
build_info = true
|
||||
extra_output = ["storageLayout"]
|
||||
build_info_path = "out/build-info"
|
||||
|
||||
# Solidity compiler settings (match Hardhat)
|
||||
solc_version = "0.8.28"
|
||||
optimizer = true
|
||||
optimizer_runs = 200
|
||||
via_ir = false
|
||||
evm_version = "cancun"
|
||||
|
||||
# File system permissions for OpenZeppelin plugin
|
||||
fs_permissions = [{ access = "read", path = "out" }]
|
||||
|
||||
# Linked libraries (deployed on Celo Mainnet)
|
||||
libraries = [
|
||||
"contracts/libraries/CustomVerifier.sol:CustomVerifier:0x9E66B82Da87309fAE1403078d498a069A30860c4",
|
||||
"node_modules/poseidon-solidity/PoseidonT3.sol:PoseidonT3:0xF134707a4C4a3a76b8410fC0294d620A7c341581"
|
||||
]
|
||||
|
||||
# Celo mainnet and testnet RPC endpoints
|
||||
[rpc_endpoints]
|
||||
celo = "${CELO_RPC_URL}"
|
||||
celo_alfajores = "https://alfajores-forno.celo-testnet.org"
|
||||
|
||||
# Etherscan API configuration for contract verification
|
||||
[etherscan]
|
||||
celo = { key = "${CELOSCAN_API_KEY}", url = "https://api.celoscan.io/api" }
|
||||
celo_alfajores = { key = "${CELOSCAN_API_KEY}", url = "https://api-alfajores.celoscan.io/api" }
|
||||
|
||||
# Formatting settings
|
||||
[fmt]
|
||||
line_length = 120
|
||||
tab_width = 4
|
||||
bracket_spacing = true
|
||||
int_types = "long"
|
||||
multiline_func_header = "all"
|
||||
quote_style = "double"
|
||||
number_underscore = "thousands"
|
||||
@@ -1,12 +1,15 @@
|
||||
import { HardhatUserConfig } from "hardhat/config";
|
||||
import "@nomicfoundation/hardhat-toolbox";
|
||||
import "@openzeppelin/hardhat-upgrades";
|
||||
import dotenv from "dotenv";
|
||||
dotenv.config();
|
||||
import "hardhat-contract-sizer";
|
||||
import "@nomicfoundation/hardhat-ignition-ethers";
|
||||
import "solidity-coverage";
|
||||
import "hardhat-gas-reporter";
|
||||
import "hardhat-contract-sizer";
|
||||
|
||||
// Import custom upgrade tasks
|
||||
import "./tasks/upgrade";
|
||||
|
||||
// Use a dummy private key for CI/local development (not used for actual deployments)
|
||||
const DUMMY_PRIVATE_KEY = "0x0000000000000000000000000000000000000000000000000000000000000001";
|
||||
@@ -16,9 +19,10 @@ const config: HardhatUserConfig = {
|
||||
solidity: {
|
||||
version: "0.8.28",
|
||||
settings: {
|
||||
evmVersion: "cancun",
|
||||
optimizer: {
|
||||
enabled: true,
|
||||
runs: 100000,
|
||||
runs: 200,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -37,7 +41,7 @@ const config: HardhatUserConfig = {
|
||||
chainId: 31337,
|
||||
url: "http://127.0.0.1:8545",
|
||||
accounts: {
|
||||
mnemonic: "test test test test test test test test test test test test",
|
||||
mnemonic: "test test test test test test test test test test test junk",
|
||||
count: 20,
|
||||
},
|
||||
},
|
||||
|
||||
1
contracts/lib/forge-std
Submodule
1
contracts/lib/forge-std
Submodule
Submodule contracts/lib/forge-std added at 8e40513d67
1
contracts/lib/openzeppelin-foundry-upgrades
Submodule
1
contracts/lib/openzeppelin-foundry-upgrades
Submodule
Submodule contracts/lib/openzeppelin-foundry-upgrades added at cbce1e0030
@@ -71,7 +71,10 @@
|
||||
"update:ofacroot": "npx dotenv-cli -- bash -c 'NETWORK=${NETWORK} npx tsx scripts/updateRegistryOfacRoot.ts'",
|
||||
"update:pcr0": "npx dotenv-cli -- bash -c 'PCR0_ACTION=${PCR0_ACTION:-add} PCR0_KEY=${PCR0_KEY} yarn hardhat ignition deploy ignition/modules/scripts/updatePCR0.ts --network ${NETWORK:-localhost} --reset'",
|
||||
"upgrade:hub": "npx dotenv-cli -- bash -c 'yarn hardhat ignition deploy ignition/modules/upgrade/deployNewHubAndUpgrade.ts --network ${NETWORK:-localhost} ${VERIFY:+--verify}'",
|
||||
"upgrade:registry": "npx dotenv-cli -- bash -c 'yarn hardhat ignition deploy ignition/modules/upgrade/deployNewRegistryAndUpgrade.ts --network ${NETWORK:-localhost} ${VERIFY:+--verify}'"
|
||||
"upgrade:registry": "npx dotenv-cli -- bash -c 'yarn hardhat ignition deploy ignition/modules/upgrade/deployNewRegistryAndUpgrade.ts --network ${NETWORK:-localhost} ${VERIFY:+--verify}'",
|
||||
"upgrade": "npx dotenv-cli -- bash -c 'yarn hardhat upgrade --network ${NETWORK:-localhost}'",
|
||||
"upgrade:status": "npx dotenv-cli -- bash -c 'yarn hardhat upgrade:status --network ${NETWORK:-localhost}'",
|
||||
"upgrade:history": "yarn hardhat upgrade:history"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ashpect/smt": "https://github.com/ashpect/smt#main",
|
||||
@@ -81,6 +84,9 @@
|
||||
"@openpassport/zk-kit-smt": "^0.0.1",
|
||||
"@openzeppelin/contracts": "5.4.0",
|
||||
"@openzeppelin/contracts-upgradeable": "5.4.0",
|
||||
"@safe-global/api-kit": "^4.0.1",
|
||||
"@safe-global/protocol-kit": "^6.1.2",
|
||||
"@safe-global/safe-core-sdk-types": "^5.1.0",
|
||||
"@selfxyz/common": "workspace:^",
|
||||
"@zk-kit/imt": "^2.0.0-beta.4",
|
||||
"@zk-kit/imt.sol": "^2.0.0-beta.12",
|
||||
@@ -103,6 +109,7 @@
|
||||
"@nomicfoundation/hardhat-toolbox": "^3.0.0",
|
||||
"@nomicfoundation/hardhat-verify": "^2.0.6",
|
||||
"@nomicfoundation/ignition-core": "^0.15.12",
|
||||
"@openzeppelin/hardhat-upgrades": "^3.9.1",
|
||||
"@typechain/ethers-v6": "^0.4.3",
|
||||
"@typechain/hardhat": "^8.0.3",
|
||||
"@types/chai": "^4.3.16",
|
||||
|
||||
4
contracts/remappings.txt
Normal file
4
contracts/remappings.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
@openzeppelin/contracts/=node_modules/@openzeppelin/contracts/
|
||||
@openzeppelin/contracts-upgradeable/=node_modules/@openzeppelin/contracts-upgradeable/
|
||||
forge-std/=lib/forge-std/src/
|
||||
openzeppelin-foundry-upgrades/=lib/openzeppelin-foundry-upgrades/src/
|
||||
157
contracts/script/MigratePCR0Manager.s.sol
Normal file
157
contracts/script/MigratePCR0Manager.s.sol
Normal file
@@ -0,0 +1,157 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity 0.8.28;
|
||||
|
||||
import {Script} from "forge-std/Script.sol";
|
||||
import {console2} from "forge-std/console2.sol";
|
||||
import {PCR0Manager} from "../contracts/utils/PCR0Manager.sol";
|
||||
|
||||
/**
|
||||
* @title MigratePCR0Manager
|
||||
* @notice Foundry script to deploy and initialize new PCR0Manager with AccessControl governance
|
||||
*
|
||||
* This script:
|
||||
* 1. Deploys new PCR0Manager (V2 with AccessControl)
|
||||
* 2. Adds all 7 finalized PCR0 values
|
||||
* 3. Transfers roles to multisigs
|
||||
* 4. Deployer renounces all roles
|
||||
* 5. Verifies final state
|
||||
*
|
||||
* Usage:
|
||||
* - Set in .env file:
|
||||
* SECURITY_GOVERNANCE_ADDRESS=0x...
|
||||
* OPERATIONS_GOVERNANCE_ADDRESS=0x...
|
||||
* - Dry run: forge script script/MigratePCR0Manager.s.sol --fork-url $CELO_RPC_URL -vvv
|
||||
* - Execute: forge script script/MigratePCR0Manager.s.sol --rpc-url https://forno.celo.org --broadcast --verify -vvv
|
||||
*/
|
||||
contract MigratePCR0Manager is Script {
|
||||
// Governance roles
|
||||
bytes32 public constant SECURITY_ROLE = keccak256("SECURITY_ROLE");
|
||||
bytes32 public constant OPERATIONS_ROLE = keccak256("OPERATIONS_ROLE");
|
||||
|
||||
// Multisig addresses (from environment)
|
||||
address securityMultisig;
|
||||
address operationsMultisig;
|
||||
|
||||
function run() external returns (address newPCR0Manager) {
|
||||
console2.log("================================================================================");
|
||||
console2.log("PCR0MANAGER DEPLOYMENT: Fresh deployment with AccessControl");
|
||||
console2.log("================================================================================");
|
||||
|
||||
console2.log("\nDeployer:", msg.sender);
|
||||
console2.log("Chain ID:", block.chainid);
|
||||
|
||||
// Get multisig addresses from .env
|
||||
securityMultisig = vm.envAddress("SECURITY_GOVERNANCE_ADDRESS");
|
||||
operationsMultisig = vm.envAddress("OPERATIONS_GOVERNANCE_ADDRESS");
|
||||
|
||||
require(securityMultisig != address(0), "SECURITY_GOVERNANCE_ADDRESS not set in .env");
|
||||
require(operationsMultisig != address(0), "OPERATIONS_GOVERNANCE_ADDRESS not set in .env");
|
||||
|
||||
console2.log("\nGovernance addresses:");
|
||||
console2.log(" Critical Multisig:", securityMultisig);
|
||||
console2.log(" Standard Multisig:", operationsMultisig);
|
||||
|
||||
// Get finalized PCR0 values
|
||||
bytes[] memory pcr0Values = getFinalizedPCR0Values();
|
||||
|
||||
console2.log("\nPCR0 values to add:", pcr0Values.length);
|
||||
|
||||
vm.startBroadcast();
|
||||
|
||||
// Step 1: Deploy PCR0Manager
|
||||
console2.log("\n=== Step 1: Deploy PCR0Manager ===");
|
||||
PCR0Manager pcr0Manager = new PCR0Manager();
|
||||
newPCR0Manager = address(pcr0Manager);
|
||||
console2.log("Deployed at:", newPCR0Manager);
|
||||
|
||||
// Step 2: Add PCR0 values
|
||||
console2.log("\n=== Step 2: Add PCR0 Values ===");
|
||||
for (uint256 i = 0; i < pcr0Values.length; i++) {
|
||||
pcr0Manager.addPCR0(pcr0Values[i]);
|
||||
console2.log(" Added PCR0", i + 1, "of", pcr0Values.length);
|
||||
}
|
||||
|
||||
// Step 3: Transfer roles to multisigs
|
||||
console2.log("\n=== Step 3: Transfer Roles to Multisigs ===");
|
||||
pcr0Manager.grantRole(SECURITY_ROLE, securityMultisig);
|
||||
pcr0Manager.grantRole(OPERATIONS_ROLE, operationsMultisig);
|
||||
console2.log(" Granted SECURITY_ROLE to:", securityMultisig);
|
||||
console2.log(" Granted OPERATIONS_ROLE to:", operationsMultisig);
|
||||
|
||||
// Step 4: Deployer renounces roles
|
||||
console2.log("\n=== Step 4: Deployer Renounces All Roles ===");
|
||||
pcr0Manager.renounceRole(SECURITY_ROLE, msg.sender);
|
||||
pcr0Manager.renounceRole(OPERATIONS_ROLE, msg.sender);
|
||||
console2.log(" Deployer renounced SECURITY_ROLE");
|
||||
console2.log(" Deployer renounced OPERATIONS_ROLE");
|
||||
|
||||
vm.stopBroadcast();
|
||||
|
||||
// Step 5: Verify final state
|
||||
console2.log("\n=== Step 5: Verify Final State ===");
|
||||
verifyFinalState(pcr0Manager, pcr0Values);
|
||||
|
||||
console2.log("\n================================================================================");
|
||||
console2.log("DEPLOYMENT COMPLETE!");
|
||||
console2.log("================================================================================");
|
||||
console2.log("\nNew PCR0Manager:", newPCR0Manager);
|
||||
console2.log("Total PCR0 values:", pcr0Values.length);
|
||||
console2.log("Governance:");
|
||||
console2.log(" Critical Multisig:", securityMultisig);
|
||||
console2.log(" Standard Multisig:", operationsMultisig);
|
||||
console2.log("\nNext steps:");
|
||||
console2.log("1. Update Hub to point to new PCR0Manager");
|
||||
console2.log("2. Update documentation with new address");
|
||||
console2.log("3. Verify contract on Celoscan");
|
||||
|
||||
return newPCR0Manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Returns finalized PCR0 values (32-byte format)
|
||||
* @dev These will be padded to 48 bytes by PCR0Manager (prefixed with 16 zero bytes)
|
||||
*/
|
||||
function getFinalizedPCR0Values() internal pure returns (bytes[] memory) {
|
||||
bytes[] memory pcr0s = new bytes[](7);
|
||||
|
||||
pcr0s[0] = hex"eb71776987d5f057030823f591d160c9d5d5e0a96c9a2a826778be1da2b8302a";
|
||||
pcr0s[1] = hex"d2221a0ee83901980c607ceff2edbedf3f6ce5f437eafa5d89be39e9e7487c04";
|
||||
pcr0s[2] = hex"4458aeb87796e92700be2d9c2984e376bce42bd80a4bf679e060d3bdaa6de119";
|
||||
pcr0s[3] = hex"aa3deefa408710420e8b4ffe5b95f1dafeb4f06cb16ea44ec7353944671c660a";
|
||||
pcr0s[4] = hex"b31e0df12cd52b961590796511d91a26364dd963c4aa727246b40513e470c232";
|
||||
pcr0s[5] = hex"26bc53c698f78016ad7c326198d25d309d1487098af3f28fc55e951f903e9596";
|
||||
pcr0s[6] = hex"b62720bdb510c2830cf9d58caa23912d0b214d6c278bf22e90942a6b69d272af";
|
||||
|
||||
return pcr0s;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Verifies the final state of the deployed PCR0Manager
|
||||
*/
|
||||
function verifyFinalState(PCR0Manager pcr0Manager, bytes[] memory pcr0Values) internal view {
|
||||
// Verify all PCR0 values are set (need 48-byte format for checking)
|
||||
for (uint256 i = 0; i < pcr0Values.length; i++) {
|
||||
// Pad to 48 bytes
|
||||
bytes memory padded = abi.encodePacked(new bytes(16), pcr0Values[i]);
|
||||
bool isSet = pcr0Manager.isPCR0Set(padded);
|
||||
require(isSet, "PCR0 value not set");
|
||||
}
|
||||
console2.log(" [PASS] All", pcr0Values.length, "PCR0 values verified");
|
||||
|
||||
// Verify deployer has no roles
|
||||
bool deployerHasCritical = pcr0Manager.hasRole(SECURITY_ROLE, msg.sender);
|
||||
bool deployerHasStandard = pcr0Manager.hasRole(OPERATIONS_ROLE, msg.sender);
|
||||
require(!deployerHasCritical, "Deployer still has SECURITY_ROLE");
|
||||
require(!deployerHasStandard, "Deployer still has OPERATIONS_ROLE");
|
||||
console2.log(" [PASS] Deployer has no roles");
|
||||
|
||||
// Verify multisigs have roles
|
||||
bool criticalHasRole = pcr0Manager.hasRole(SECURITY_ROLE, securityMultisig);
|
||||
bool standardHasRole = pcr0Manager.hasRole(OPERATIONS_ROLE, operationsMultisig);
|
||||
require(criticalHasRole, "Critical multisig missing SECURITY_ROLE");
|
||||
require(standardHasRole, "Standard multisig missing OPERATIONS_ROLE");
|
||||
console2.log(" [PASS] Multisigs have correct roles");
|
||||
|
||||
console2.log("\n [SUCCESS] All verifications passed!");
|
||||
}
|
||||
}
|
||||
179
contracts/tasks/upgrade/README.md
Normal file
179
contracts/tasks/upgrade/README.md
Normal file
@@ -0,0 +1,179 @@
|
||||
# Upgrade Tooling
|
||||
|
||||
A comprehensive toolset for safely upgrading UUPS proxy contracts in the Self Protocol.
|
||||
|
||||
## Overview
|
||||
|
||||
The upgrade tooling provides:
|
||||
|
||||
- **Safety checks** - Storage layout validation, version validation, reinitializer verification
|
||||
- **Safe multisig integration** - Creates proposals for SECURITY_ROLE approval
|
||||
- **Version tracking** - Automatic registry updates and git tagging
|
||||
- **Audit trail** - Complete deployment history with changelogs
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Single command to validate, deploy, and propose
|
||||
npx hardhat upgrade --contract IdentityVerificationHub --network celo --changelog "Added feature X"
|
||||
```
|
||||
|
||||
## The `upgrade` Command
|
||||
|
||||
Validates, deploys, and creates a Safe multisig proposal in one step.
|
||||
|
||||
```bash
|
||||
npx hardhat upgrade \
|
||||
--contract <ContractId> \
|
||||
--network <network> \
|
||||
[--changelog <message>] \
|
||||
[--prepare-only]
|
||||
```
|
||||
|
||||
**Options:**
|
||||
|
||||
- `--contract` - Contract to upgrade (IdentityVerificationHub, IdentityRegistry, etc.)
|
||||
- `--network` - Target network (celo, sepolia, localhost)
|
||||
- `--changelog` - Description of changes
|
||||
- `--prepare-only` - Deploy implementation without creating Safe proposal
|
||||
|
||||
**What it does:**
|
||||
|
||||
1. ✅ Validates `@custom:version` increment
|
||||
2. ✅ Checks `reinitializer(N)` matches expected version
|
||||
3. ✅ Validates storage layout compatibility
|
||||
4. ✅ Clears cache and compiles fresh
|
||||
5. ✅ Compares bytecode (warns if unchanged)
|
||||
6. ✅ Deploys new implementation
|
||||
7. ✅ Updates deployment registry
|
||||
8. ✅ Creates git commit and tag
|
||||
9. ✅ Creates Safe proposal (or outputs manual instructions)
|
||||
|
||||
## Utility Commands
|
||||
|
||||
```bash
|
||||
# Check current deployment status
|
||||
npx hardhat upgrade:status --contract IdentityVerificationHub --network celo
|
||||
|
||||
# View version history
|
||||
npx hardhat upgrade:history --contract IdentityVerificationHub
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
### For Developers
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ 1. UPDATE CONTRACT CODE │
|
||||
│ - Make your changes │
|
||||
│ - Update @custom:version in NatSpec │
|
||||
│ - Increment reinitializer(N) modifier │
|
||||
│ - Add new storage fields at END of struct only │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ 2. RUN: npx hardhat upgrade --contract X --network Y --changelog Z │
|
||||
│ - Validates all safety checks │
|
||||
│ - Deploys new implementation │
|
||||
│ - Updates registry.json │
|
||||
│ - Creates git commit + tag │
|
||||
│ - Creates Safe proposal │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ 3. MULTISIG APPROVAL │
|
||||
│ - Signers review in Safe UI │
|
||||
│ - Once threshold met, click Execute │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Contract Update Pattern
|
||||
|
||||
```solidity
|
||||
/**
|
||||
* @title MyContract
|
||||
* @custom:version 2.13.0 // <-- Update this
|
||||
*/
|
||||
contract MyContract is ImplRoot {
|
||||
|
||||
struct MyStorage {
|
||||
uint256 existingField;
|
||||
uint256 newField; // <-- Add new fields at end only
|
||||
}
|
||||
|
||||
// Increment reinitializer(N) for each upgrade
|
||||
function initialize(...) external reinitializer(13) {
|
||||
// Initialize new fields if needed
|
||||
MyStorage storage $ = _getMyStorage();
|
||||
if ($.newField == 0) {
|
||||
$.newField = defaultValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Deployment Registry
|
||||
|
||||
The registry (`deployments/registry.json`) tracks:
|
||||
|
||||
- Proxy addresses per network
|
||||
- Current versions
|
||||
- Implementation history
|
||||
- Git commits and tags
|
||||
|
||||
### Governance Configuration
|
||||
|
||||
Multisig addresses are configured in `deployments/registry.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"networks": {
|
||||
"celo": {
|
||||
"governance": {
|
||||
"securityMultisig": "0x...",
|
||||
"operationsMultisig": "0x...",
|
||||
"securityThreshold": "3/5",
|
||||
"operationsThreshold": "2/5"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Required for deployments:
|
||||
|
||||
```bash
|
||||
PRIVATE_KEY=0x... # Deployer private key
|
||||
CELO_RPC_URL=https://... # RPC endpoint
|
||||
```
|
||||
|
||||
## Supported Contracts
|
||||
|
||||
| Contract ID | Contract Name | Type |
|
||||
| ------------------------- | ----------------------------- | ---------- |
|
||||
| `IdentityVerificationHub` | IdentityVerificationHubImplV2 | UUPS Proxy |
|
||||
| `IdentityRegistry` | IdentityRegistryImplV1 | UUPS Proxy |
|
||||
| `IdentityRegistryIdCard` | IdentityRegistryIdCardImplV1 | UUPS Proxy |
|
||||
| `IdentityRegistryAadhaar` | IdentityRegistryAadhaarImplV1 | UUPS Proxy |
|
||||
|
||||
## Safety Checks
|
||||
|
||||
| Check | What it Does | Failure Behavior |
|
||||
| ---------------------- | ------------------------------------------- | -------------------- |
|
||||
| Version validation | Ensures semantic version increment | Blocks upgrade |
|
||||
| Reinitializer check | Verifies `reinitializer(N)` matches version | Blocks upgrade |
|
||||
| Storage layout | Detects breaking storage changes | Blocks upgrade |
|
||||
| Bytecode comparison | Warns if code unchanged | Prompts confirmation |
|
||||
| Safe role verification | Confirms Safe has SECURITY_ROLE | Blocks upgrade |
|
||||
| Constructor check | Flags `_disableInitializers()` | Prompts confirmation |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Issue | Solution |
|
||||
| ----------------------------- | ----------------------------------------- |
|
||||
| "Version matches current" | Update `@custom:version` in contract |
|
||||
| "Reinitializer mismatch" | Update `reinitializer(N)` to next version |
|
||||
| "Storage layout incompatible" | Don't remove/reorder storage variables |
|
||||
| "Safe not indexed" | Submit manually via Safe UI |
|
||||
| "Bytecode unchanged" | Ensure you saved contract changes |
|
||||
91
contracts/tasks/upgrade/history.ts
Normal file
91
contracts/tasks/upgrade/history.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* upgrade:history task
|
||||
*
|
||||
* Shows deployment history for a contract.
|
||||
*/
|
||||
|
||||
import { task, types } from "hardhat/config";
|
||||
import { log, readRegistry, getContractDefinition, compareVersions, shortenAddress } from "./utils";
|
||||
import { CONTRACT_IDS, ContractId } from "./types";
|
||||
|
||||
interface HistoryTaskArgs {
|
||||
contract: ContractId;
|
||||
}
|
||||
|
||||
task("upgrade:history", "Show deployment history for a contract")
|
||||
.addParam("contract", `Contract to show history for (${CONTRACT_IDS.join(", ")})`, undefined, types.string)
|
||||
.setAction(async (args: HistoryTaskArgs) => {
|
||||
const { contract: contractId } = args;
|
||||
|
||||
log.header(`DEPLOYMENT HISTORY: ${contractId}`);
|
||||
|
||||
if (!CONTRACT_IDS.includes(contractId as ContractId)) {
|
||||
log.error(`Invalid contract: ${contractId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const registry = readRegistry();
|
||||
const contractDef = getContractDefinition(contractId);
|
||||
const versions = registry.versions[contractId] || {};
|
||||
|
||||
// Contract info
|
||||
console.log("\n📋 Contract Information");
|
||||
console.log("─".repeat(60));
|
||||
log.detail("Source", contractDef.source);
|
||||
log.detail("Type", contractDef.type);
|
||||
log.detail("Description", contractDef.description);
|
||||
|
||||
// Network deployments
|
||||
console.log("\n🔗 Network Deployments");
|
||||
console.log("─".repeat(60));
|
||||
|
||||
for (const [networkName, networkConfig] of Object.entries(registry.networks)) {
|
||||
const deployment = networkConfig.deployments[contractId];
|
||||
if (deployment) {
|
||||
const address = deployment.proxy || deployment.address || "Not deployed";
|
||||
const version = deployment.currentVersion || "N/A";
|
||||
console.log(` ${networkName.padEnd(15)} ${shortenAddress(address).padEnd(15)} v${version}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Version history
|
||||
console.log("\n📜 Version History");
|
||||
console.log("─".repeat(60));
|
||||
|
||||
const versionNumbers = Object.keys(versions).sort((a, b) => compareVersions(b, a));
|
||||
|
||||
if (versionNumbers.length === 0) {
|
||||
console.log(" No versions recorded");
|
||||
}
|
||||
|
||||
for (const version of versionNumbers) {
|
||||
const info = versions[version];
|
||||
const isCurrent = Object.values(registry.networks).some(
|
||||
(n) => n.deployments[contractId]?.currentVersion === version,
|
||||
);
|
||||
|
||||
console.log(`\n ${isCurrent ? "→" : " "} v${version} (Initializer v${info.initializerVersion})`);
|
||||
if (isCurrent) {
|
||||
console.log(" CURRENT");
|
||||
}
|
||||
console.log(" " + "─".repeat(50));
|
||||
console.log(` Changelog: ${info.changelog}`);
|
||||
console.log(` Initializer: ${info.initializerFunction}()`);
|
||||
console.log(` Git tag: ${info.gitTag}`);
|
||||
|
||||
// Show deployments per network
|
||||
if (info.deployments && Object.keys(info.deployments).length > 0) {
|
||||
console.log(" Deployments:");
|
||||
for (const [network, deployment] of Object.entries(info.deployments)) {
|
||||
if (deployment.impl) {
|
||||
const date = deployment.deployedAt ? new Date(deployment.deployedAt).toLocaleString() : "Unknown";
|
||||
console.log(` ${network}: ${shortenAddress(deployment.impl)} (${date})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log("\n");
|
||||
});
|
||||
|
||||
export {};
|
||||
14
contracts/tasks/upgrade/index.ts
Normal file
14
contracts/tasks/upgrade/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Upgrade Tasks
|
||||
*
|
||||
* Main entry point for contract upgrade tooling.
|
||||
*/
|
||||
|
||||
// Import all upgrade tasks
|
||||
import "./upgrade";
|
||||
import "./prepare";
|
||||
import "./propose";
|
||||
import "./status";
|
||||
import "./history";
|
||||
|
||||
export {};
|
||||
403
contracts/tasks/upgrade/prepare.ts
Normal file
403
contracts/tasks/upgrade/prepare.ts
Normal file
@@ -0,0 +1,403 @@
|
||||
/**
|
||||
* upgrade:prepare task
|
||||
*
|
||||
* Validates and deploys a new implementation contract.
|
||||
* Does NOT execute the upgrade - that requires multisig approval.
|
||||
*
|
||||
* Features:
|
||||
* - Auto-validates version increment (must be current + 1)
|
||||
* - Auto-updates @custom:version in contract if needed
|
||||
* - Auto-commits after successful deployment
|
||||
* - Records git commit hash and creates tag
|
||||
*
|
||||
* Usage:
|
||||
* npx hardhat upgrade:prepare --contract IdentityVerificationHub --network celo
|
||||
*/
|
||||
|
||||
import { task, types } from "hardhat/config";
|
||||
import { HardhatRuntimeEnvironment } from "hardhat/types";
|
||||
import {
|
||||
log,
|
||||
getContractDefinition,
|
||||
getProxyAddress,
|
||||
getCurrentVersion,
|
||||
getGitCommitShort,
|
||||
getGitBranch,
|
||||
hasUncommittedChanges,
|
||||
validateVersionIncrement,
|
||||
suggestNextVersion,
|
||||
readContractVersion,
|
||||
updateContractVersion,
|
||||
getContractFilePath,
|
||||
addVersion,
|
||||
getExplorerUrl,
|
||||
shortenAddress,
|
||||
createGitTag,
|
||||
gitCommit,
|
||||
getLatestVersionInfo,
|
||||
} from "./utils";
|
||||
import { CONTRACT_IDS, ContractId, SupportedNetwork } from "./types";
|
||||
|
||||
interface PrepareTaskArgs {
|
||||
contract: ContractId;
|
||||
newVersion?: string;
|
||||
changelog?: string;
|
||||
dryRun: boolean;
|
||||
skipCommit: boolean;
|
||||
}
|
||||
|
||||
task("upgrade:prepare", "Validate and deploy a new implementation contract")
|
||||
.addParam("contract", `Contract to upgrade (${CONTRACT_IDS.join(", ")})`, undefined, types.string)
|
||||
.addOptionalParam(
|
||||
"newVersion",
|
||||
"New version - auto-detected from contract file if not provided",
|
||||
undefined,
|
||||
types.string,
|
||||
)
|
||||
.addOptionalParam("changelog", "Changelog entry for this version", undefined, types.string)
|
||||
.addFlag("dryRun", "Simulate the deployment without actually deploying")
|
||||
.addFlag("skipCommit", "Skip auto-commit after deployment")
|
||||
.setAction(async (args: PrepareTaskArgs, hre: HardhatRuntimeEnvironment) => {
|
||||
const { contract: contractId, changelog, dryRun, skipCommit } = args;
|
||||
let { newVersion } = args;
|
||||
const network = hre.network.name as SupportedNetwork;
|
||||
|
||||
log.header(`UPGRADE PREPARE: ${contractId}`);
|
||||
log.detail("Network", network);
|
||||
log.detail("Mode", dryRun ? "DRY RUN (no actual deployment)" : "LIVE DEPLOYMENT");
|
||||
|
||||
// ========================================================================
|
||||
// Step 1: Validate inputs
|
||||
// ========================================================================
|
||||
log.step("Validating inputs...");
|
||||
|
||||
if (!CONTRACT_IDS.includes(contractId as ContractId)) {
|
||||
log.error(`Invalid contract: ${contractId}`);
|
||||
log.info(`Valid contracts: ${CONTRACT_IDS.join(", ")}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const contractDef = getContractDefinition(contractId);
|
||||
if (contractDef.type !== "uups-proxy") {
|
||||
log.error(`Contract '${contractId}' is not upgradeable (type: ${contractDef.type})`);
|
||||
return;
|
||||
}
|
||||
|
||||
let proxyAddress: string;
|
||||
try {
|
||||
proxyAddress = getProxyAddress(contractId, network);
|
||||
} catch {
|
||||
log.error(`No proxy deployed for '${contractId}' on network '${network}'`);
|
||||
log.info("Deploy the proxy first using the deploy script");
|
||||
return;
|
||||
}
|
||||
|
||||
const currentVersion = getCurrentVersion(contractId, network);
|
||||
|
||||
log.detail("Contract source", contractDef.source);
|
||||
log.detail("Proxy address", proxyAddress);
|
||||
log.detail("Current version", currentVersion);
|
||||
|
||||
// ========================================================================
|
||||
// Step 2: Determine and validate new version
|
||||
// ========================================================================
|
||||
log.step("Validating version...");
|
||||
|
||||
const contractFilePath = getContractFilePath(contractId);
|
||||
const contractFileVersion = contractFilePath ? readContractVersion(contractFilePath) : null;
|
||||
|
||||
if (contractFileVersion) {
|
||||
log.detail("Version in contract file", contractFileVersion);
|
||||
}
|
||||
|
||||
// If no version provided, use contract file version
|
||||
if (!newVersion) {
|
||||
if (contractFileVersion && contractFileVersion !== currentVersion) {
|
||||
const validation = validateVersionIncrement(currentVersion, contractFileVersion);
|
||||
if (validation.valid) {
|
||||
newVersion = contractFileVersion;
|
||||
log.info(`Using version from contract file: ${newVersion}`);
|
||||
} else {
|
||||
log.error(`Contract file has invalid version ${contractFileVersion}`);
|
||||
const suggestions = suggestNextVersion(currentVersion);
|
||||
log.info(`Current version: ${currentVersion}`);
|
||||
log.info(
|
||||
`Valid next versions: ${suggestions.patch} (patch), ${suggestions.minor} (minor), ${suggestions.major} (major)`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
log.error("Contract file version matches current - update @custom:version in contract first");
|
||||
const suggestions = suggestNextVersion(currentVersion);
|
||||
log.info(`Current version: ${currentVersion}`);
|
||||
log.info(
|
||||
`Valid next versions: ${suggestions.patch} (patch), ${suggestions.minor} (minor), ${suggestions.major} (major)`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate version increment
|
||||
const versionValidation = validateVersionIncrement(currentVersion, newVersion);
|
||||
if (!versionValidation.valid) {
|
||||
log.error(versionValidation.error!);
|
||||
return;
|
||||
}
|
||||
|
||||
log.success(`Version increment valid: ${currentVersion} → ${newVersion} (${versionValidation.type})`);
|
||||
|
||||
// ========================================================================
|
||||
// Step 3: Update contract file version if needed
|
||||
// ========================================================================
|
||||
if (contractFilePath && contractFileVersion !== newVersion) {
|
||||
log.step("Updating contract file version...");
|
||||
|
||||
if (dryRun) {
|
||||
log.info(`[DRY RUN] Would update ${contractFilePath} to version ${newVersion}`);
|
||||
} else {
|
||||
const updated = updateContractVersion(contractFilePath, newVersion);
|
||||
if (updated) {
|
||||
log.success(`Updated @custom:version to ${newVersion} in contract file`);
|
||||
} else {
|
||||
log.warning("Could not update version in contract file - please update manually");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Step 4: Check git state
|
||||
// ========================================================================
|
||||
log.step("Checking git state...");
|
||||
|
||||
const gitBranch = getGitBranch();
|
||||
const uncommittedChanges = hasUncommittedChanges();
|
||||
|
||||
log.detail("Branch", gitBranch);
|
||||
|
||||
if (uncommittedChanges && !dryRun) {
|
||||
log.warning("You have uncommitted changes. They will be included in the auto-commit.");
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Step 5: Load and validate the new implementation
|
||||
// ========================================================================
|
||||
log.step("Loading contract factory...");
|
||||
|
||||
const contractName = contractDef.source;
|
||||
let ContractFactory;
|
||||
|
||||
try {
|
||||
// Handle contracts that need library linking
|
||||
if (contractName === "IdentityVerificationHubImplV2") {
|
||||
const CustomVerifier = await hre.ethers.getContractFactory("CustomVerifier");
|
||||
const customVerifier = await CustomVerifier.deploy();
|
||||
await customVerifier.waitForDeployment();
|
||||
|
||||
ContractFactory = await hre.ethers.getContractFactory(contractName, {
|
||||
libraries: {
|
||||
CustomVerifier: await customVerifier.getAddress(),
|
||||
},
|
||||
});
|
||||
log.info("Deployed CustomVerifier library for linking");
|
||||
} else if (
|
||||
contractName === "IdentityRegistryImplV1" ||
|
||||
contractName === "IdentityRegistryIdCardImplV1" ||
|
||||
contractName === "IdentityRegistryAadhaarImplV1"
|
||||
) {
|
||||
const PoseidonT3 = await hre.ethers.getContractFactory("PoseidonT3");
|
||||
const poseidonT3 = await PoseidonT3.deploy();
|
||||
await poseidonT3.waitForDeployment();
|
||||
|
||||
ContractFactory = await hre.ethers.getContractFactory(contractName, {
|
||||
libraries: {
|
||||
PoseidonT3: await poseidonT3.getAddress(),
|
||||
},
|
||||
});
|
||||
log.info("Deployed PoseidonT3 library for linking");
|
||||
} else {
|
||||
ContractFactory = await hre.ethers.getContractFactory(contractName);
|
||||
}
|
||||
|
||||
log.success(`Loaded contract factory: ${contractName}`);
|
||||
} catch (error) {
|
||||
log.error(`Failed to load contract factory: ${error}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Step 6: Validate storage layout
|
||||
// ========================================================================
|
||||
log.step("Validating storage layout compatibility...");
|
||||
|
||||
try {
|
||||
await hre.upgrades.validateImplementation(ContractFactory, {
|
||||
kind: "uups",
|
||||
unsafeAllowLinkedLibraries: true,
|
||||
unsafeAllow: ["constructor", "external-library-linking"],
|
||||
});
|
||||
log.success("Storage layout validation passed");
|
||||
} catch (error) {
|
||||
log.error(`Storage layout validation failed: ${error}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Step 7: Simulate upgrade on fork
|
||||
// ========================================================================
|
||||
log.step("Simulating upgrade on fork...");
|
||||
|
||||
try {
|
||||
const proxyContract = await hre.ethers.getContractAt(contractName, proxyAddress);
|
||||
const SECURITY_ROLE = await proxyContract.SECURITY_ROLE();
|
||||
log.detail("SECURITY_ROLE", SECURITY_ROLE);
|
||||
log.success("Fork simulation passed - proxy is accessible");
|
||||
} catch (error) {
|
||||
log.error(`Fork simulation failed: ${error}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Step 8: Dry run summary
|
||||
// ========================================================================
|
||||
if (dryRun) {
|
||||
log.step("DRY RUN - Skipping actual deployment");
|
||||
log.box([
|
||||
"DRY RUN SUMMARY",
|
||||
"─".repeat(50),
|
||||
`Contract: ${contractId}`,
|
||||
`Version: ${currentVersion} → ${newVersion}`,
|
||||
`Network: ${network}`,
|
||||
`Proxy: ${proxyAddress}`,
|
||||
"",
|
||||
"What would happen:",
|
||||
`1. Update contract file to version ${newVersion}`,
|
||||
"2. Deploy new implementation",
|
||||
"3. Update registry.json",
|
||||
"4. Create git commit and tag",
|
||||
"",
|
||||
"Run without --dry-run to execute.",
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Step 9: Deploy new implementation
|
||||
// ========================================================================
|
||||
log.step("Deploying new implementation...");
|
||||
|
||||
try {
|
||||
const implementation = await ContractFactory.deploy();
|
||||
await implementation.waitForDeployment();
|
||||
const implementationAddress = await implementation.getAddress();
|
||||
|
||||
log.success(`Implementation deployed: ${implementationAddress}`);
|
||||
log.detail("Explorer", `${getExplorerUrl(network)}/address/${implementationAddress}`);
|
||||
|
||||
// ========================================================================
|
||||
// Step 10: Verify on block explorer
|
||||
// ========================================================================
|
||||
log.step("Verifying contract on block explorer...");
|
||||
|
||||
try {
|
||||
await hre.run("verify:verify", {
|
||||
address: implementationAddress,
|
||||
constructorArguments: [],
|
||||
});
|
||||
log.success("Contract verified on block explorer");
|
||||
} catch (error: any) {
|
||||
if (error.message?.includes("Already Verified")) {
|
||||
log.info("Contract already verified");
|
||||
} else {
|
||||
log.warning(`Verification failed: ${error.message}`);
|
||||
log.info("You can verify manually later");
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Step 11: Update registry
|
||||
// ========================================================================
|
||||
log.step("Updating deployment registry...");
|
||||
|
||||
// Get previous version info to determine initializer version
|
||||
const latestVersion = getLatestVersionInfo(contractId);
|
||||
const newInitializerVersion = (latestVersion?.info.initializerVersion || 0) + 1;
|
||||
|
||||
const gitCommitShort = getGitCommitShort();
|
||||
const deployerAddress = (await hre.ethers.provider.getSigner()).address;
|
||||
|
||||
addVersion(
|
||||
contractId,
|
||||
network,
|
||||
newVersion,
|
||||
{
|
||||
initializerVersion: newInitializerVersion,
|
||||
initializerFunction: newInitializerVersion === 1 ? "initialize" : `initializeV${newInitializerVersion}`,
|
||||
changelog: changelog || `Upgrade to v${newVersion}`,
|
||||
gitTag: `${contractId.toLowerCase()}-v${newVersion}`,
|
||||
},
|
||||
{
|
||||
impl: implementationAddress,
|
||||
deployedAt: new Date().toISOString(),
|
||||
deployedBy: deployerAddress,
|
||||
gitCommit: "", // Will be set after commit
|
||||
},
|
||||
);
|
||||
log.success("Registry updated");
|
||||
|
||||
// ========================================================================
|
||||
// Step 12: Auto-commit and tag
|
||||
// ========================================================================
|
||||
if (!skipCommit) {
|
||||
log.step("Creating git commit...");
|
||||
|
||||
const commitMessage = `feat: ${contractId} v${newVersion} deployed on ${network.charAt(0).toUpperCase() + network.slice(1)}
|
||||
|
||||
- Implementation: ${implementationAddress}
|
||||
- Changelog: ${changelog || "Upgrade"}`;
|
||||
|
||||
const committed = gitCommit(commitMessage);
|
||||
if (committed) {
|
||||
const newGitCommit = getGitCommitShort();
|
||||
log.success(`Committed: ${newGitCommit}`);
|
||||
|
||||
// Try to create git tag
|
||||
try {
|
||||
createGitTag(
|
||||
`${contractId.toLowerCase()}-v${newVersion}`,
|
||||
`${contractId} v${newVersion} - ${changelog || "Upgrade"}`,
|
||||
);
|
||||
log.success(`Created git tag: ${contractId.toLowerCase()}-v${newVersion}`);
|
||||
} catch (e) {
|
||||
log.warning("Could not create git tag - you can create it manually");
|
||||
}
|
||||
} else {
|
||||
log.warning("Could not create git commit - please commit manually");
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Summary
|
||||
// ========================================================================
|
||||
log.box([
|
||||
"DEPLOYMENT SUCCESSFUL",
|
||||
"═".repeat(50),
|
||||
`Contract: ${contractId}`,
|
||||
`Version: ${currentVersion} → ${newVersion}`,
|
||||
`Network: ${network}`,
|
||||
"",
|
||||
"Addresses:",
|
||||
` Proxy: ${shortenAddress(proxyAddress)}`,
|
||||
` New Impl: ${shortenAddress(implementationAddress)}`,
|
||||
"",
|
||||
"Next steps:",
|
||||
` 1. Run: npx hardhat upgrade:propose --contract ${contractId} --network ${network}`,
|
||||
" 2. Multisig signers approve in Safe UI",
|
||||
" 3. Transaction executes automatically when threshold reached",
|
||||
]);
|
||||
} catch (error) {
|
||||
log.error(`Deployment failed: ${error}`);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
export {};
|
||||
201
contracts/tasks/upgrade/propose.ts
Normal file
201
contracts/tasks/upgrade/propose.ts
Normal file
@@ -0,0 +1,201 @@
|
||||
/**
|
||||
* upgrade:propose task
|
||||
*
|
||||
* Creates a Safe multisig transaction to execute the upgrade.
|
||||
* The implementation must already be deployed via upgrade:prepare.
|
||||
*
|
||||
* Usage:
|
||||
* npx hardhat upgrade:propose --contract IdentityVerificationHub --network celo
|
||||
*/
|
||||
|
||||
import { task, types } from "hardhat/config";
|
||||
import { HardhatRuntimeEnvironment } from "hardhat/types";
|
||||
import {
|
||||
log,
|
||||
getContractDefinition,
|
||||
getProxyAddress,
|
||||
getCurrentVersion,
|
||||
getGovernanceConfig,
|
||||
getVersionInfo,
|
||||
getNetworkDeployment,
|
||||
} from "./utils";
|
||||
import { CONTRACT_IDS, ContractId, SupportedNetwork } from "./types";
|
||||
|
||||
/**
|
||||
* Get Safe chain prefix for URL
|
||||
*/
|
||||
function getChainPrefix(network: SupportedNetwork): string {
|
||||
const prefixes: Record<SupportedNetwork, string> = {
|
||||
celo: "celo",
|
||||
"celo-sepolia": "celo",
|
||||
sepolia: "sep",
|
||||
localhost: "eth",
|
||||
};
|
||||
return prefixes[network] || network;
|
||||
}
|
||||
|
||||
interface ProposeTaskArgs {
|
||||
contract: ContractId;
|
||||
dryRun: boolean;
|
||||
}
|
||||
|
||||
task("upgrade:propose", "Create Safe transaction to execute the upgrade")
|
||||
.addParam("contract", `Contract to upgrade (${CONTRACT_IDS.join(", ")})`, undefined, types.string)
|
||||
.addFlag("dryRun", "Generate transaction data without submitting to Safe")
|
||||
.setAction(async (args: ProposeTaskArgs, hre: HardhatRuntimeEnvironment) => {
|
||||
const { contract: contractId, dryRun } = args;
|
||||
const network = hre.network.name as SupportedNetwork;
|
||||
|
||||
log.header(`UPGRADE PROPOSE: ${contractId}`);
|
||||
log.detail("Network", network);
|
||||
log.detail("Mode", dryRun ? "DRY RUN (no Safe submission)" : "LIVE SUBMISSION");
|
||||
|
||||
// ========================================================================
|
||||
// Step 1: Validate inputs
|
||||
// ========================================================================
|
||||
log.step("Validating inputs...");
|
||||
|
||||
if (!CONTRACT_IDS.includes(contractId as ContractId)) {
|
||||
log.error(`Invalid contract: ${contractId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const contractDef = getContractDefinition(contractId);
|
||||
if (contractDef.type !== "uups-proxy") {
|
||||
log.error(`Contract '${contractId}' is not upgradeable`);
|
||||
return;
|
||||
}
|
||||
|
||||
let proxyAddress: string;
|
||||
try {
|
||||
proxyAddress = getProxyAddress(contractId, network);
|
||||
} catch {
|
||||
log.error(`No proxy deployed for '${contractId}' on '${network}'`);
|
||||
return;
|
||||
}
|
||||
|
||||
const currentVersion = getCurrentVersion(contractId, network);
|
||||
const deployment = getNetworkDeployment(contractId, network);
|
||||
const newImplAddress = deployment?.currentImpl;
|
||||
|
||||
if (!newImplAddress) {
|
||||
log.error("No implementation deployed. Run upgrade:prepare first.");
|
||||
return;
|
||||
}
|
||||
|
||||
const versionInfo = getVersionInfo(contractId, currentVersion);
|
||||
|
||||
log.detail("Contract", contractId);
|
||||
log.detail("Proxy", proxyAddress);
|
||||
log.detail("Current version", currentVersion);
|
||||
log.detail("New implementation", newImplAddress);
|
||||
log.detail("Changelog", versionInfo?.changelog || "N/A");
|
||||
|
||||
// ========================================================================
|
||||
// Step 2: Load governance configuration
|
||||
// ========================================================================
|
||||
log.step("Loading governance configuration...");
|
||||
|
||||
const governance = getGovernanceConfig(network);
|
||||
if (!governance.securityMultisig) {
|
||||
log.error(`No security multisig configured for network '${network}'`);
|
||||
log.info("Update deployments/registry.json with governance addresses");
|
||||
return;
|
||||
}
|
||||
|
||||
log.detail("Security multisig", governance.securityMultisig);
|
||||
log.detail("Required threshold", governance.securityThreshold);
|
||||
|
||||
// ========================================================================
|
||||
// Step 3: Verify implementation contract
|
||||
// ========================================================================
|
||||
log.step("Verifying implementation contract...");
|
||||
|
||||
const implCode = await hre.ethers.provider.getCode(newImplAddress);
|
||||
if (implCode === "0x") {
|
||||
log.error(`No contract found at implementation address ${newImplAddress}`);
|
||||
return;
|
||||
}
|
||||
log.success("Implementation contract verified on-chain");
|
||||
|
||||
// ========================================================================
|
||||
// Step 4: Build upgrade transaction
|
||||
// ========================================================================
|
||||
log.step("Building upgrade transaction...");
|
||||
|
||||
const contractName = contractDef.source;
|
||||
const proxyContract = await hre.ethers.getContractAt(contractName, proxyAddress);
|
||||
|
||||
// Check if there's an initializer to call
|
||||
let initData = "0x";
|
||||
const initializerName = versionInfo?.initializerFunction;
|
||||
if (initializerName && initializerName !== "initialize") {
|
||||
try {
|
||||
const iface = proxyContract.interface;
|
||||
const initFragment = iface.getFunction(initializerName);
|
||||
if (initFragment) {
|
||||
initData = iface.encodeFunctionData(initializerName, []);
|
||||
log.detail("Initialization", initializerName);
|
||||
}
|
||||
} catch {
|
||||
log.detail("Initialization", "None (function not found)");
|
||||
}
|
||||
} else {
|
||||
log.detail("Initialization", "None");
|
||||
}
|
||||
|
||||
// Encode upgradeToAndCall
|
||||
const upgradeData = proxyContract.interface.encodeFunctionData("upgradeToAndCall", [newImplAddress, initData]);
|
||||
|
||||
log.detail("Method", "upgradeToAndCall(address,bytes)");
|
||||
log.detail("Target", proxyAddress);
|
||||
|
||||
// ========================================================================
|
||||
// Step 5: Output transaction data
|
||||
// ========================================================================
|
||||
if (dryRun) {
|
||||
log.step("DRY RUN - Transaction data generated");
|
||||
} else {
|
||||
log.step("Generating Safe proposal data...");
|
||||
}
|
||||
|
||||
const chainPrefix = getChainPrefix(network);
|
||||
|
||||
log.success("Transaction data generated");
|
||||
|
||||
log.box([
|
||||
"SAFE PROPOSAL READY",
|
||||
"═".repeat(60),
|
||||
`Safe: ${governance.securityMultisig}`,
|
||||
`Threshold: ${governance.securityThreshold}`,
|
||||
"",
|
||||
"Transaction:",
|
||||
` To: ${proxyAddress}`,
|
||||
" Value: 0",
|
||||
` Data: ${upgradeData.slice(0, 50)}...`,
|
||||
"",
|
||||
"Submit via Safe UI:",
|
||||
` 1. Go to: https://app.safe.global/home?safe=${chainPrefix}:${governance.securityMultisig}`,
|
||||
" 2. Click 'New transaction' → 'Transaction Builder'",
|
||||
" 3. Enter: To, Value, Data from above",
|
||||
` 4. ${governance.securityThreshold} signers approve`,
|
||||
" 5. Upgrade executes automatically!",
|
||||
]);
|
||||
|
||||
// Output raw data for copy-paste
|
||||
console.log("\n📋 Raw transaction data (copy this for Transaction Builder):");
|
||||
console.log("─".repeat(60));
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
to: proxyAddress,
|
||||
value: "0",
|
||||
data: upgradeData,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
export {};
|
||||
113
contracts/tasks/upgrade/status.ts
Normal file
113
contracts/tasks/upgrade/status.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* upgrade:status task
|
||||
*
|
||||
* Shows current deployment status for a contract.
|
||||
*/
|
||||
|
||||
import { task, types } from "hardhat/config";
|
||||
import { HardhatRuntimeEnvironment } from "hardhat/types";
|
||||
import {
|
||||
log,
|
||||
getContractDefinition,
|
||||
getNetworkDeployment,
|
||||
getGovernanceConfig,
|
||||
getVersionInfo,
|
||||
shortenAddress,
|
||||
getExplorerUrl,
|
||||
} from "./utils";
|
||||
import { CONTRACT_IDS, ContractId, SupportedNetwork } from "./types";
|
||||
|
||||
interface StatusTaskArgs {
|
||||
contract: ContractId;
|
||||
}
|
||||
|
||||
task("upgrade:status", "Show current deployment status for a contract")
|
||||
.addParam("contract", `Contract to check (${CONTRACT_IDS.join(", ")})`, undefined, types.string)
|
||||
.setAction(async (args: StatusTaskArgs, hre: HardhatRuntimeEnvironment) => {
|
||||
const { contract: contractId } = args;
|
||||
const network = hre.network.name as SupportedNetwork;
|
||||
|
||||
log.header(`STATUS: ${contractId} on ${network}`);
|
||||
|
||||
if (!CONTRACT_IDS.includes(contractId as ContractId)) {
|
||||
log.error(`Invalid contract: ${contractId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const contractDef = getContractDefinition(contractId);
|
||||
const deployment = getNetworkDeployment(contractId, network);
|
||||
const governance = getGovernanceConfig(network);
|
||||
|
||||
console.log("\n📋 Contract Info");
|
||||
console.log("─".repeat(60));
|
||||
log.detail("Source", contractDef.source);
|
||||
log.detail("Type", contractDef.type);
|
||||
|
||||
if (!deployment) {
|
||||
log.warning(`Not deployed on ${network}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const currentVersion = deployment.currentVersion;
|
||||
const proxyAddress = deployment.proxy || deployment.address;
|
||||
const implAddress = deployment.currentImpl;
|
||||
const versionInfo = currentVersion ? getVersionInfo(contractId, currentVersion) : null;
|
||||
|
||||
console.log("\n🔗 Deployment");
|
||||
console.log("─".repeat(60));
|
||||
log.detail("Proxy", proxyAddress || "N/A");
|
||||
log.detail("Implementation", implAddress || "N/A");
|
||||
log.detail("Current version", currentVersion || "N/A");
|
||||
if (proxyAddress) {
|
||||
log.detail("Explorer", `${getExplorerUrl(network)}/address/${proxyAddress}`);
|
||||
}
|
||||
|
||||
if (versionInfo) {
|
||||
console.log("\n📌 Version Info");
|
||||
console.log("─".repeat(60));
|
||||
log.detail("Changelog", versionInfo.changelog);
|
||||
log.detail("Initializer", versionInfo.initializerFunction);
|
||||
log.detail("Git tag", versionInfo.gitTag);
|
||||
}
|
||||
|
||||
console.log("\n🔐 Governance");
|
||||
console.log("─".repeat(60));
|
||||
log.detail("Security multisig", governance.securityMultisig || "Not configured");
|
||||
log.detail("Security threshold", governance.securityThreshold);
|
||||
log.detail("Operations multisig", governance.operationsMultisig || "Not configured");
|
||||
log.detail("Operations threshold", governance.operationsThreshold);
|
||||
|
||||
// Check on-chain state if connected
|
||||
if (proxyAddress && contractDef.type === "uups-proxy") {
|
||||
try {
|
||||
const proxy = await hre.ethers.getContractAt(contractDef.source, proxyAddress);
|
||||
|
||||
console.log("\n⛓️ On-Chain State");
|
||||
console.log("─".repeat(60));
|
||||
|
||||
// Try to read version
|
||||
try {
|
||||
const onChainVersion = await proxy.version();
|
||||
log.detail("On-chain version", onChainVersion);
|
||||
} catch {
|
||||
log.detail("On-chain version", "N/A");
|
||||
}
|
||||
|
||||
// Check role holders
|
||||
try {
|
||||
const SECURITY_ROLE = await proxy.SECURITY_ROLE();
|
||||
const OPERATIONS_ROLE = await proxy.OPERATIONS_ROLE();
|
||||
log.detail("SECURITY_ROLE", shortenAddress(SECURITY_ROLE));
|
||||
log.detail("OPERATIONS_ROLE", shortenAddress(OPERATIONS_ROLE));
|
||||
} catch {
|
||||
// Not all contracts have these
|
||||
}
|
||||
} catch (error) {
|
||||
log.warning(`Could not read on-chain state: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("\n");
|
||||
});
|
||||
|
||||
export {};
|
||||
29
contracts/tasks/upgrade/types.ts
Normal file
29
contracts/tasks/upgrade/types.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Types for upgrade tooling
|
||||
*/
|
||||
|
||||
export const SUPPORTED_NETWORKS = ["celo", "celo-sepolia", "sepolia", "localhost"] as const;
|
||||
export type SupportedNetwork = (typeof SUPPORTED_NETWORKS)[number];
|
||||
|
||||
// Contract IDs match registry keys
|
||||
export const CONTRACT_IDS = [
|
||||
"IdentityVerificationHub",
|
||||
"IdentityRegistry",
|
||||
"IdentityRegistryIdCard",
|
||||
"IdentityRegistryAadhaar",
|
||||
"PCR0Manager",
|
||||
"VerifyAll",
|
||||
"DummyContract",
|
||||
] as const;
|
||||
export type ContractId = (typeof CONTRACT_IDS)[number];
|
||||
|
||||
// Re-export types from utils for convenience
|
||||
export type {
|
||||
ContractDefinition,
|
||||
NetworkConfig,
|
||||
NetworkDeployment,
|
||||
GovernanceConfig,
|
||||
VersionInfo,
|
||||
VersionDeployment,
|
||||
DeploymentRegistry,
|
||||
} from "./utils";
|
||||
1029
contracts/tasks/upgrade/upgrade.ts
Normal file
1029
contracts/tasks/upgrade/upgrade.ts
Normal file
File diff suppressed because it is too large
Load Diff
631
contracts/tasks/upgrade/utils.ts
Normal file
631
contracts/tasks/upgrade/utils.ts
Normal file
@@ -0,0 +1,631 @@
|
||||
/**
|
||||
* Utility functions for upgrade tooling
|
||||
*
|
||||
* Works with the new registry structure:
|
||||
* - contracts: Contract definitions (source, type, description)
|
||||
* - networks: Per-network deployments and governance
|
||||
* - versions: Version history with deployment details
|
||||
*/
|
||||
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { execSync } from "child_process";
|
||||
import { SupportedNetwork } from "./types";
|
||||
|
||||
// Registry types matching the new structure
|
||||
export interface ContractDefinition {
|
||||
source: string;
|
||||
type: "uups-proxy" | "non-upgradeable";
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface NetworkDeployment {
|
||||
proxy?: string;
|
||||
address?: string;
|
||||
currentVersion: string;
|
||||
currentImpl?: string;
|
||||
}
|
||||
|
||||
export interface GovernanceConfig {
|
||||
securityMultisig: string;
|
||||
operationsMultisig: string;
|
||||
securityThreshold: string;
|
||||
operationsThreshold: string;
|
||||
}
|
||||
|
||||
export interface NetworkConfig {
|
||||
chainId: number;
|
||||
governance: GovernanceConfig;
|
||||
deployments: Record<string, NetworkDeployment>;
|
||||
}
|
||||
|
||||
export interface VersionDeployment {
|
||||
impl: string;
|
||||
deployedAt: string;
|
||||
deployedBy: string;
|
||||
gitCommit: string;
|
||||
}
|
||||
|
||||
export interface VersionInfo {
|
||||
initializerVersion: number;
|
||||
initializerFunction: string;
|
||||
changelog: string;
|
||||
gitTag: string;
|
||||
deployments: Record<string, VersionDeployment>;
|
||||
}
|
||||
|
||||
export interface DeploymentRegistry {
|
||||
$schema: string;
|
||||
lastUpdated: string;
|
||||
contracts: Record<string, ContractDefinition>;
|
||||
networks: Record<string, NetworkConfig>;
|
||||
versions: Record<string, Record<string, VersionInfo>>;
|
||||
}
|
||||
|
||||
// Console colors for pretty output
|
||||
const colors = {
|
||||
reset: "\x1b[0m",
|
||||
bold: "\x1b[1m",
|
||||
red: "\x1b[31m",
|
||||
green: "\x1b[32m",
|
||||
yellow: "\x1b[33m",
|
||||
blue: "\x1b[34m",
|
||||
magenta: "\x1b[35m",
|
||||
cyan: "\x1b[36m",
|
||||
white: "\x1b[37m",
|
||||
gray: "\x1b[90m",
|
||||
};
|
||||
|
||||
export const log = {
|
||||
info: (msg: string) => console.log(`${colors.blue}ℹ${colors.reset} ${msg}`),
|
||||
success: (msg: string) => console.log(`${colors.green}✅${colors.reset} ${msg}`),
|
||||
warning: (msg: string) => console.log(`${colors.yellow}⚠️${colors.reset} ${msg}`),
|
||||
error: (msg: string) => console.log(`${colors.red}❌${colors.reset} ${msg}`),
|
||||
step: (msg: string) => console.log(`\n${colors.magenta}🔄${colors.reset} ${colors.bold}${msg}${colors.reset}`),
|
||||
header: (msg: string) =>
|
||||
console.log(
|
||||
`\n${colors.cyan}${"═".repeat(70)}${colors.reset}\n${colors.bold}${msg}${colors.reset}\n${colors.cyan}${"═".repeat(70)}${colors.reset}`,
|
||||
),
|
||||
detail: (label: string, value: string) => console.log(` ${colors.gray}${label}:${colors.reset} ${value}`),
|
||||
box: (lines: string[]) => {
|
||||
const maxLen = Math.max(...lines.map((l) => l.length));
|
||||
console.log(`\n┌${"─".repeat(maxLen + 2)}┐`);
|
||||
lines.forEach((line) => console.log(`│ ${line.padEnd(maxLen)} │`));
|
||||
console.log(`└${"─".repeat(maxLen + 2)}┘\n`);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the path to the deployment registry
|
||||
*/
|
||||
export function getRegistryPath(): string {
|
||||
return path.join(__dirname, "../../deployments/registry.json");
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the deployment registry
|
||||
*/
|
||||
export function readRegistry(): DeploymentRegistry {
|
||||
const registryPath = getRegistryPath();
|
||||
if (!fs.existsSync(registryPath)) {
|
||||
throw new Error(`Deployment registry not found at ${registryPath}`);
|
||||
}
|
||||
return JSON.parse(fs.readFileSync(registryPath, "utf-8"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the deployment registry
|
||||
*/
|
||||
export function writeRegistry(registry: DeploymentRegistry): void {
|
||||
const registryPath = getRegistryPath();
|
||||
registry.lastUpdated = new Date().toISOString();
|
||||
fs.writeFileSync(registryPath, JSON.stringify(registry, null, 2) + "\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get contract definition from registry
|
||||
*/
|
||||
export function getContractDefinition(contractId: string): ContractDefinition {
|
||||
const registry = readRegistry();
|
||||
const contract = registry.contracts[contractId];
|
||||
if (!contract) {
|
||||
throw new Error(`Contract '${contractId}' not found in registry`);
|
||||
}
|
||||
return contract;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get network config
|
||||
*/
|
||||
export function getNetworkConfig(network: SupportedNetwork): NetworkConfig {
|
||||
const registry = readRegistry();
|
||||
const networkConfig = registry.networks[network];
|
||||
if (!networkConfig) {
|
||||
throw new Error(`Network '${network}' not configured in registry`);
|
||||
}
|
||||
return networkConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get deployment for a contract on a network
|
||||
*/
|
||||
export function getNetworkDeployment(contractId: string, network: SupportedNetwork): NetworkDeployment | null {
|
||||
const registry = readRegistry();
|
||||
return registry.networks[network]?.deployments?.[contractId] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get proxy address for a contract on a network
|
||||
*/
|
||||
export function getProxyAddress(contractId: string, network: SupportedNetwork): string {
|
||||
const deployment = getNetworkDeployment(contractId, network);
|
||||
if (!deployment?.proxy) {
|
||||
throw new Error(`No proxy address found for '${contractId}' on network '${network}'`);
|
||||
}
|
||||
return deployment.proxy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current version for a contract on a network
|
||||
*/
|
||||
export function getCurrentVersion(contractId: string, network: SupportedNetwork): string {
|
||||
const deployment = getNetworkDeployment(contractId, network);
|
||||
return deployment?.currentVersion || "0.0.0";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get governance config for a network
|
||||
*/
|
||||
export function getGovernanceConfig(network: SupportedNetwork): GovernanceConfig {
|
||||
const networkConfig = getNetworkConfig(network);
|
||||
return networkConfig.governance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get version info from registry
|
||||
*/
|
||||
export function getVersionInfo(contractId: string, version: string): VersionInfo | null {
|
||||
const registry = readRegistry();
|
||||
return registry.versions[contractId]?.[version] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get latest version info for a contract
|
||||
*/
|
||||
export function getLatestVersionInfo(contractId: string): { version: string; info: VersionInfo } | null {
|
||||
const registry = readRegistry();
|
||||
const versions = registry.versions[contractId];
|
||||
if (!versions) return null;
|
||||
|
||||
const versionNumbers = Object.keys(versions).sort((a, b) => compareVersions(b, a));
|
||||
if (versionNumbers.length === 0) return null;
|
||||
|
||||
return { version: versionNumbers[0], info: versions[versionNumbers[0]] };
|
||||
}
|
||||
|
||||
/**
|
||||
* Add new version to registry
|
||||
*/
|
||||
export function addVersion(
|
||||
contractId: string,
|
||||
network: SupportedNetwork,
|
||||
version: string,
|
||||
versionInfo: Omit<VersionInfo, "deployments">,
|
||||
deployment: VersionDeployment,
|
||||
): void {
|
||||
const registry = readRegistry();
|
||||
|
||||
// Initialize versions object if needed
|
||||
if (!registry.versions[contractId]) {
|
||||
registry.versions[contractId] = {};
|
||||
}
|
||||
|
||||
// Add or update version info
|
||||
if (!registry.versions[contractId][version]) {
|
||||
registry.versions[contractId][version] = {
|
||||
...versionInfo,
|
||||
deployments: {},
|
||||
};
|
||||
}
|
||||
registry.versions[contractId][version].deployments[network] = deployment;
|
||||
|
||||
// Update network deployment
|
||||
if (!registry.networks[network]) {
|
||||
throw new Error(`Network '${network}' not configured`);
|
||||
}
|
||||
if (!registry.networks[network].deployments[contractId]) {
|
||||
registry.networks[network].deployments[contractId] = {
|
||||
proxy: "",
|
||||
currentVersion: "",
|
||||
currentImpl: "",
|
||||
};
|
||||
}
|
||||
registry.networks[network].deployments[contractId].currentVersion = version;
|
||||
registry.networks[network].deployments[contractId].currentImpl = deployment.impl;
|
||||
|
||||
writeRegistry(registry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update gitCommit for a specific version deployment
|
||||
*/
|
||||
export function updateVersionGitCommit(
|
||||
contractId: string,
|
||||
network: SupportedNetwork,
|
||||
version: string,
|
||||
gitCommit: string,
|
||||
): void {
|
||||
const registry = readRegistry();
|
||||
|
||||
if (!registry.versions[contractId]?.[version]?.deployments?.[network]) {
|
||||
throw new Error(`Deployment not found: ${contractId} v${version} on ${network}`);
|
||||
}
|
||||
|
||||
registry.versions[contractId][version].deployments[network].gitCommit = gitCommit;
|
||||
writeRegistry(registry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update proxy address for a contract on a network
|
||||
*/
|
||||
export function setProxyAddress(contractId: string, network: SupportedNetwork, proxyAddress: string): void {
|
||||
const registry = readRegistry();
|
||||
|
||||
if (!registry.networks[network]) {
|
||||
throw new Error(`Network '${network}' not configured`);
|
||||
}
|
||||
if (!registry.networks[network].deployments[contractId]) {
|
||||
registry.networks[network].deployments[contractId] = {
|
||||
proxy: "",
|
||||
currentVersion: "",
|
||||
currentImpl: "",
|
||||
};
|
||||
}
|
||||
registry.networks[network].deployments[contractId].proxy = proxyAddress;
|
||||
|
||||
writeRegistry(registry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current git commit hash
|
||||
*/
|
||||
export function getGitCommit(): string {
|
||||
try {
|
||||
return execSync("git rev-parse HEAD").toString().trim();
|
||||
} catch {
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get short git commit hash
|
||||
*/
|
||||
export function getGitCommitShort(): string {
|
||||
try {
|
||||
return execSync("git rev-parse --short HEAD").toString().trim();
|
||||
} catch {
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current git branch
|
||||
*/
|
||||
export function getGitBranch(): string {
|
||||
try {
|
||||
return execSync("git rev-parse --abbrev-ref HEAD").toString().trim();
|
||||
} catch {
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there are uncommitted changes
|
||||
*/
|
||||
export function hasUncommittedChanges(): boolean {
|
||||
try {
|
||||
const status = execSync("git status --porcelain").toString().trim();
|
||||
return status.length > 0;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a git tag
|
||||
*/
|
||||
export function createGitTag(tag: string, message: string): void {
|
||||
execSync(`git tag -a ${tag} -m "${message}"`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse semantic version
|
||||
*/
|
||||
export function parseVersion(version: string): { major: number; minor: number; patch: number } {
|
||||
const match = version.match(/^(\d+)\.(\d+)\.(\d+)$/);
|
||||
if (!match) {
|
||||
throw new Error(`Invalid version format: ${version}`);
|
||||
}
|
||||
return {
|
||||
major: parseInt(match[1]),
|
||||
minor: parseInt(match[2]),
|
||||
patch: parseInt(match[3]),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare versions (returns 1 if a > b, -1 if a < b, 0 if equal)
|
||||
*/
|
||||
export function compareVersions(a: string, b: string): number {
|
||||
const va = parseVersion(a);
|
||||
const vb = parseVersion(b);
|
||||
|
||||
if (va.major !== vb.major) return va.major > vb.major ? 1 : -1;
|
||||
if (va.minor !== vb.minor) return va.minor > vb.minor ? 1 : -1;
|
||||
if (va.patch !== vb.patch) return va.patch > vb.patch ? 1 : -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment version
|
||||
*/
|
||||
export function incrementVersion(version: string, type: "major" | "minor" | "patch"): string {
|
||||
const v = parseVersion(version);
|
||||
switch (type) {
|
||||
case "major":
|
||||
return `${v.major + 1}.0.0`;
|
||||
case "minor":
|
||||
return `${v.major}.${v.minor + 1}.0`;
|
||||
case "patch":
|
||||
return `${v.major}.${v.minor}.${v.patch + 1}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Suggest next version based on current version
|
||||
*/
|
||||
export function suggestNextVersion(currentVersion: string): {
|
||||
patch: string;
|
||||
minor: string;
|
||||
major: string;
|
||||
} {
|
||||
const v = parseVersion(currentVersion);
|
||||
return {
|
||||
patch: `${v.major}.${v.minor}.${v.patch + 1}`,
|
||||
minor: `${v.major}.${v.minor + 1}.0`,
|
||||
major: `${v.major + 1}.0.0`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that new version is a valid increment of current version
|
||||
*/
|
||||
export function validateVersionIncrement(
|
||||
currentVersion: string,
|
||||
newVersion: string,
|
||||
): {
|
||||
valid: boolean;
|
||||
type: "patch" | "minor" | "major" | null;
|
||||
error?: string;
|
||||
} {
|
||||
try {
|
||||
const current = parseVersion(currentVersion);
|
||||
const next = parseVersion(newVersion);
|
||||
|
||||
// Check if it's a valid increment
|
||||
if (next.major === current.major + 1 && next.minor === 0 && next.patch === 0) {
|
||||
return { valid: true, type: "major" };
|
||||
}
|
||||
if (next.major === current.major && next.minor === current.minor + 1 && next.patch === 0) {
|
||||
return { valid: true, type: "minor" };
|
||||
}
|
||||
if (next.major === current.major && next.minor === current.minor && next.patch === current.patch + 1) {
|
||||
return { valid: true, type: "patch" };
|
||||
}
|
||||
|
||||
// Not a valid increment
|
||||
const suggested = suggestNextVersion(currentVersion);
|
||||
return {
|
||||
valid: false,
|
||||
type: null,
|
||||
error: `Invalid version increment. Current: ${currentVersion}, Got: ${newVersion}. Valid options: ${suggested.patch} (patch), ${suggested.minor} (minor), ${suggested.major} (major)`,
|
||||
};
|
||||
} catch (e) {
|
||||
return { valid: false, type: null, error: `Invalid version format: ${e}` };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read version from contract file's @custom:version
|
||||
*/
|
||||
export function readContractVersion(contractPath: string): string | null {
|
||||
try {
|
||||
const content = fs.readFileSync(contractPath, "utf-8");
|
||||
const match = content.match(/@custom:version\s+(\d+\.\d+\.\d+)/);
|
||||
return match ? match[1] : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read reinitializer version from contract's initialize function
|
||||
* Looks for patterns like: reinitializer(N) or initializer
|
||||
* Returns the highest reinitializer version found, or 1 if only initializer is found
|
||||
*/
|
||||
export function readReinitializerVersion(contractPath: string): number | null {
|
||||
try {
|
||||
const content = fs.readFileSync(contractPath, "utf-8");
|
||||
|
||||
// Find all reinitializer(N) occurrences
|
||||
const reinitMatches = content.matchAll(/reinitializer\s*\(\s*(\d+)\s*\)/g);
|
||||
const versions: number[] = [];
|
||||
|
||||
for (const match of reinitMatches) {
|
||||
versions.push(parseInt(match[1]));
|
||||
}
|
||||
|
||||
if (versions.length > 0) {
|
||||
// Return the highest version found
|
||||
return Math.max(...versions);
|
||||
}
|
||||
|
||||
// Check for basic initializer modifier (equivalent to reinitializer(1))
|
||||
if (content.match(/\binitializer\b/) && !content.match(/reinitializer/)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that reinitializer version matches expected version
|
||||
* Expected version = previous initializer version + 1
|
||||
*/
|
||||
export function validateReinitializerVersion(
|
||||
contractPath: string,
|
||||
expectedVersion: number,
|
||||
): { valid: boolean; actual: number | null; error?: string } {
|
||||
const actual = readReinitializerVersion(contractPath);
|
||||
|
||||
if (actual === null) {
|
||||
return {
|
||||
valid: false,
|
||||
actual: null,
|
||||
error: "Could not find reinitializer/initializer modifier in contract",
|
||||
};
|
||||
}
|
||||
|
||||
if (actual !== expectedVersion) {
|
||||
return {
|
||||
valid: false,
|
||||
actual,
|
||||
error: `Reinitializer version mismatch. Expected: reinitializer(${expectedVersion}), Found: reinitializer(${actual})`,
|
||||
};
|
||||
}
|
||||
|
||||
return { valid: true, actual };
|
||||
}
|
||||
|
||||
/**
|
||||
* Update version in contract file's @custom:version
|
||||
*/
|
||||
export function updateContractVersion(contractPath: string, newVersion: string): boolean {
|
||||
try {
|
||||
let content = fs.readFileSync(contractPath, "utf-8");
|
||||
const originalContent = content;
|
||||
|
||||
// Update @custom:version
|
||||
content = content.replace(/@custom:version\s+\d+\.\d+\.\d+/, `@custom:version ${newVersion}`);
|
||||
|
||||
// Also update version() function if it exists
|
||||
content = content.replace(/function version\(\)[^}]+return\s+"(\d+\.\d+\.\d+)"/, (match) =>
|
||||
match.replace(/"\d+\.\d+\.\d+"/, `"${newVersion}"`),
|
||||
);
|
||||
|
||||
if (content !== originalContent) {
|
||||
fs.writeFileSync(contractPath, content);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get contract file path from contract ID
|
||||
*/
|
||||
export function getContractFilePath(contractId: string): string | null {
|
||||
const contract = getContractDefinition(contractId);
|
||||
const contractName = contract.source;
|
||||
|
||||
// Common paths to check
|
||||
const possiblePaths = [
|
||||
path.join(__dirname, `../../contracts/${contractName}.sol`),
|
||||
path.join(__dirname, `../../contracts/tests/${contractName}.sol`),
|
||||
path.join(__dirname, `../../contracts/registry/${contractName}.sol`),
|
||||
path.join(__dirname, `../../contracts/utils/${contractName}.sol`),
|
||||
path.join(__dirname, `../../contracts/sdk/${contractName}.sol`),
|
||||
];
|
||||
|
||||
for (const p of possiblePaths) {
|
||||
if (fs.existsSync(p)) {
|
||||
return p;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a git commit
|
||||
*/
|
||||
export function gitCommit(message: string): boolean {
|
||||
try {
|
||||
execSync(`git add -A && git commit -m "${message}"`, { stdio: "pipe" });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format address for display (shortened)
|
||||
*/
|
||||
export function shortenAddress(address: string): string {
|
||||
if (!address || address.length < 10) return address;
|
||||
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Safe API URL for a network
|
||||
*/
|
||||
export function getSafeApiUrl(network: SupportedNetwork): string {
|
||||
// Safe Transaction Service API URLs (must end with /api/)
|
||||
const urls: Record<SupportedNetwork, string> = {
|
||||
celo: "https://safe-transaction-celo.safe.global/api/",
|
||||
"celo-sepolia": "https://safe-transaction-celo.safe.global/api/", // Celo testnet uses same as mainnet
|
||||
sepolia: "https://safe-transaction-sepolia.safe.global/api/",
|
||||
localhost: "", // No Safe service for localhost
|
||||
};
|
||||
return urls[network];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get block explorer URL for a network
|
||||
*/
|
||||
export function getExplorerUrl(network: SupportedNetwork): string {
|
||||
const urls: Record<SupportedNetwork, string> = {
|
||||
celo: "https://celoscan.io",
|
||||
"celo-sepolia": "https://celo-sepolia.blockscout.com",
|
||||
sepolia: "https://sepolia.etherscan.io",
|
||||
localhost: "http://localhost:8545", // No explorer for localhost
|
||||
};
|
||||
return urls[network];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all contract IDs from registry
|
||||
*/
|
||||
export function getContractIds(): string[] {
|
||||
const registry = readRegistry();
|
||||
return Object.keys(registry.contracts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if contract is deployed on network
|
||||
*/
|
||||
export function isDeployedOnNetwork(contractId: string, network: SupportedNetwork): boolean {
|
||||
const deployment = getNetworkDeployment(contractId, network);
|
||||
if (!deployment) return false;
|
||||
|
||||
const contract = getContractDefinition(contractId);
|
||||
if (contract.type === "uups-proxy") {
|
||||
return !!deployment.proxy;
|
||||
}
|
||||
return !!deployment.address;
|
||||
}
|
||||
158
contracts/test/foundry/MigratePCR0Manager.t.sol
Normal file
158
contracts/test/foundry/MigratePCR0Manager.t.sol
Normal file
@@ -0,0 +1,158 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity 0.8.28;
|
||||
|
||||
import {Test} from "forge-std/Test.sol";
|
||||
import {console2} from "forge-std/console2.sol";
|
||||
import {PCR0Manager} from "../../contracts/utils/PCR0Manager.sol";
|
||||
|
||||
/**
|
||||
* @title MigratePCR0ManagerTest
|
||||
* @notice Test for deploying PCR0Manager V2 with AccessControl governance
|
||||
*
|
||||
* This test:
|
||||
* 1. Deploys new PCR0Manager with AccessControl
|
||||
* 2. Adds all 7 finalized PCR0 values
|
||||
* 3. Transfers roles to multisigs
|
||||
* 4. Verifies deployer has no control after transfer
|
||||
*
|
||||
* Run with:
|
||||
* forge test --match-contract MigratePCR0ManagerTest -vvv
|
||||
*/
|
||||
contract MigratePCR0ManagerTest is Test {
|
||||
// Test accounts
|
||||
address deployer;
|
||||
address securityMultisig;
|
||||
address operationsMultisig;
|
||||
address unauthorized;
|
||||
|
||||
// Contracts
|
||||
PCR0Manager pcr0Manager;
|
||||
|
||||
// Governance roles
|
||||
bytes32 public constant SECURITY_ROLE = keccak256("SECURITY_ROLE");
|
||||
bytes32 public constant OPERATIONS_ROLE = keccak256("OPERATIONS_ROLE");
|
||||
|
||||
// Finalized PCR0 values (32-byte format)
|
||||
bytes[] pcr0Values;
|
||||
|
||||
function setUp() public {
|
||||
console2.log("================================================================================");
|
||||
console2.log("PCR0Manager DEPLOYMENT TEST: AccessControl Governance");
|
||||
console2.log("================================================================================");
|
||||
|
||||
// Set up test accounts
|
||||
deployer = makeAddr("deployer");
|
||||
securityMultisig = makeAddr("securityMultisig");
|
||||
operationsMultisig = makeAddr("operationsMultisig");
|
||||
unauthorized = makeAddr("unauthorized");
|
||||
|
||||
vm.deal(deployer, 100 ether);
|
||||
|
||||
console2.log("Deployer:", deployer);
|
||||
console2.log("Critical Multisig:", securityMultisig);
|
||||
console2.log("Standard Multisig:", operationsMultisig);
|
||||
|
||||
// Populate finalized PCR0 values (32-byte format)
|
||||
pcr0Values.push(hex"eb71776987d5f057030823f591d160c9d5d5e0a96c9a2a826778be1da2b8302a");
|
||||
pcr0Values.push(hex"d2221a0ee83901980c607ceff2edbedf3f6ce5f437eafa5d89be39e9e7487c04");
|
||||
pcr0Values.push(hex"4458aeb87796e92700be2d9c2984e376bce42bd80a4bf679e060d3bdaa6de119");
|
||||
pcr0Values.push(hex"aa3deefa408710420e8b4ffe5b95f1dafeb4f06cb16ea44ec7353944671c660a");
|
||||
pcr0Values.push(hex"b31e0df12cd52b961590796511d91a26364dd963c4aa727246b40513e470c232");
|
||||
pcr0Values.push(hex"26bc53c698f78016ad7c326198d25d309d1487098af3f28fc55e951f903e9596");
|
||||
pcr0Values.push(hex"b62720bdb510c2830cf9d58caa23912d0b214d6c278bf22e90942a6b69d272af");
|
||||
}
|
||||
|
||||
function testDeploymentWorkflow() public {
|
||||
console2.log("\n=== Step 1: Deploy PCR0Manager ===");
|
||||
|
||||
vm.startPrank(deployer);
|
||||
pcr0Manager = new PCR0Manager();
|
||||
vm.stopPrank();
|
||||
|
||||
console2.log("Deployed at:", address(pcr0Manager));
|
||||
assertTrue(pcr0Manager.hasRole(SECURITY_ROLE, deployer), "Deployer missing SECURITY_ROLE");
|
||||
assertTrue(pcr0Manager.hasRole(OPERATIONS_ROLE, deployer), "Deployer missing OPERATIONS_ROLE");
|
||||
|
||||
console2.log("\n=== Step 2: Add PCR0 Values ===");
|
||||
|
||||
// Add PCR0 values
|
||||
vm.startPrank(deployer);
|
||||
for (uint256 i = 0; i < pcr0Values.length; i++) {
|
||||
pcr0Manager.addPCR0(pcr0Values[i]);
|
||||
}
|
||||
vm.stopPrank();
|
||||
|
||||
console2.log("Added", pcr0Values.length, "PCR0 values");
|
||||
|
||||
// Verify all PCR0s are set (check with 48-byte format)
|
||||
for (uint256 i = 0; i < pcr0Values.length; i++) {
|
||||
bytes memory pcr0_48 = abi.encodePacked(new bytes(16), pcr0Values[i]);
|
||||
assertTrue(pcr0Manager.isPCR0Set(pcr0_48), "PCR0 not set");
|
||||
}
|
||||
|
||||
console2.log("\n=== Step 3: Test Governance ===");
|
||||
|
||||
// Test add/remove functionality
|
||||
vm.startPrank(deployer);
|
||||
bytes memory testPCR0_32 = hex"1111111111111111111111111111111111111111111111111111111111111111";
|
||||
bytes memory testPCR0_48 = abi.encodePacked(new bytes(16), testPCR0_32);
|
||||
pcr0Manager.addPCR0(testPCR0_32);
|
||||
assertTrue(pcr0Manager.isPCR0Set(testPCR0_48), "Test PCR0 not added");
|
||||
pcr0Manager.removePCR0(testPCR0_32);
|
||||
assertFalse(pcr0Manager.isPCR0Set(testPCR0_48), "Test PCR0 not removed");
|
||||
vm.stopPrank();
|
||||
|
||||
// Unauthorized user blocked
|
||||
vm.startPrank(unauthorized);
|
||||
vm.expectRevert();
|
||||
pcr0Manager.addPCR0(testPCR0_32);
|
||||
vm.stopPrank();
|
||||
|
||||
console2.log("Governance working correctly");
|
||||
|
||||
console2.log("\n=== Step 4: Transfer Roles to Multisigs ===");
|
||||
|
||||
vm.startPrank(deployer);
|
||||
pcr0Manager.grantRole(SECURITY_ROLE, securityMultisig);
|
||||
pcr0Manager.grantRole(OPERATIONS_ROLE, operationsMultisig);
|
||||
pcr0Manager.renounceRole(SECURITY_ROLE, deployer);
|
||||
pcr0Manager.renounceRole(OPERATIONS_ROLE, deployer);
|
||||
vm.stopPrank();
|
||||
|
||||
console2.log("Roles transferred to multisigs");
|
||||
|
||||
console2.log("\n=== Step 5: Verify Final State ===");
|
||||
|
||||
// Deployer has no roles
|
||||
assertFalse(pcr0Manager.hasRole(SECURITY_ROLE, deployer), "Deployer still has SECURITY_ROLE");
|
||||
assertFalse(pcr0Manager.hasRole(OPERATIONS_ROLE, deployer), "Deployer still has OPERATIONS_ROLE");
|
||||
|
||||
// Multisigs have roles
|
||||
assertTrue(pcr0Manager.hasRole(SECURITY_ROLE, securityMultisig), "Critical multisig missing SECURITY_ROLE");
|
||||
assertTrue(
|
||||
pcr0Manager.hasRole(OPERATIONS_ROLE, operationsMultisig),
|
||||
"Standard multisig missing OPERATIONS_ROLE"
|
||||
);
|
||||
|
||||
// Multisig can manage, deployer cannot
|
||||
vm.startPrank(securityMultisig);
|
||||
bytes memory testPCR0_32_v2 = hex"2222222222222222222222222222222222222222222222222222222222222222";
|
||||
bytes memory testPCR0_48_v2 = abi.encodePacked(new bytes(16), testPCR0_32_v2);
|
||||
pcr0Manager.addPCR0(testPCR0_32_v2);
|
||||
assertTrue(pcr0Manager.isPCR0Set(testPCR0_48_v2), "Multisig cannot add PCR0");
|
||||
pcr0Manager.removePCR0(testPCR0_32_v2);
|
||||
vm.stopPrank();
|
||||
|
||||
vm.startPrank(deployer);
|
||||
vm.expectRevert();
|
||||
pcr0Manager.addPCR0(testPCR0_32_v2);
|
||||
vm.stopPrank();
|
||||
|
||||
console2.log("Multisigs have full control");
|
||||
console2.log("Deployer has ZERO control");
|
||||
|
||||
console2.log("\n================================================================================");
|
||||
console2.log("DEPLOYMENT TEST PASSED - Ready for production");
|
||||
console2.log("================================================================================");
|
||||
}
|
||||
}
|
||||
355
contracts/test/foundry/UpgradeToAccessControl.t.sol
Normal file
355
contracts/test/foundry/UpgradeToAccessControl.t.sol
Normal file
@@ -0,0 +1,355 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity 0.8.28;
|
||||
|
||||
import {Test} from "forge-std/Test.sol";
|
||||
import {console2} from "forge-std/console2.sol";
|
||||
import {Upgrades, Options} from "openzeppelin-foundry-upgrades/Upgrades.sol";
|
||||
import {IdentityVerificationHubImplV2} from "../../contracts/IdentityVerificationHubImplV2.sol";
|
||||
import {IdentityRegistryImplV1} from "../../contracts/registry/IdentityRegistryImplV1.sol";
|
||||
import {IdentityRegistryIdCardImplV1} from "../../contracts/registry/IdentityRegistryIdCardImplV1.sol";
|
||||
import {IdentityRegistryAadhaarImplV1} from "../../contracts/registry/IdentityRegistryAadhaarImplV1.sol";
|
||||
import {Ownable2StepUpgradeable} from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol";
|
||||
|
||||
/**
|
||||
* @title UpgradeToAccessControlTest
|
||||
* @notice Fork test for upgrading contracts from Ownable to AccessControl
|
||||
*
|
||||
* This test:
|
||||
* 1. Forks Celo mainnet at current block
|
||||
* 2. Captures pre-upgrade state from real deployed contracts
|
||||
* 3. Executes upgrades to AccessControl governance
|
||||
* 4. Verifies ALL state is preserved (no storage collisions)
|
||||
* 5. Tests governance functionality
|
||||
* 6. Simulates role transfer to multisigs
|
||||
* 7. Verifies deployer has no control after transfer
|
||||
*
|
||||
* Run with:
|
||||
* forge test --match-contract UpgradeToAccessControlTest --fork-url $CELO_RPC_URL -vvv
|
||||
*/
|
||||
contract UpgradeToAccessControlTest is Test {
|
||||
// ============================================================================
|
||||
// DEPLOYED CONTRACT ADDRESSES (Celo Mainnet)
|
||||
// ============================================================================
|
||||
|
||||
address constant HUB_PROXY = 0xe57F4773bd9c9d8b6Cd70431117d353298B9f5BF;
|
||||
address constant REGISTRY_PASSPORT_PROXY = 0x37F5CB8cB1f6B00aa768D8aA99F1A9289802A968;
|
||||
address constant REGISTRY_ID_CARD_PROXY = 0xeAD1E6Ec29c1f3D33a0662f253a3a94D189566E1;
|
||||
address constant REGISTRY_AADHAAR_PROXY = 0xd603Fa8C8f4694E8DD1DcE1f27C0C3fc91e32Ac4;
|
||||
address constant CUSTOM_VERIFIER = 0x9E66B82Da87309fAE1403078d498a069A30860c4;
|
||||
address constant POSEIDON_T3 = 0xF134707a4C4a3a76b8410fC0294d620A7c341581;
|
||||
|
||||
// Test accounts
|
||||
address deployer;
|
||||
address securityMultisig;
|
||||
address operationsMultisig;
|
||||
|
||||
// Contracts
|
||||
IdentityVerificationHubImplV2 hub;
|
||||
IdentityRegistryImplV1 passportRegistry;
|
||||
IdentityRegistryIdCardImplV1 idCardRegistry;
|
||||
IdentityRegistryAadhaarImplV1 aadhaarRegistry;
|
||||
|
||||
// Governance roles
|
||||
bytes32 public constant SECURITY_ROLE = keccak256("SECURITY_ROLE");
|
||||
bytes32 public constant OPERATIONS_ROLE = keccak256("OPERATIONS_ROLE");
|
||||
|
||||
// Pre-upgrade state - captures ALL publicly accessible critical state variables
|
||||
struct PreUpgradeState {
|
||||
// Hub state (4 variables)
|
||||
address hubRegistryPassport;
|
||||
address hubRegistryIdCard;
|
||||
address hubRegistryAadhaar;
|
||||
uint256 hubAadhaarWindow;
|
||||
// Passport Registry state (6 variables)
|
||||
uint256 passportIdentityRoot;
|
||||
uint256 passportDscKeyRoot;
|
||||
uint256 passportPassportNoOfacRoot;
|
||||
uint256 passportNameDobOfacRoot;
|
||||
uint256 passportNameYobOfacRoot;
|
||||
uint256 passportCscaRoot;
|
||||
// ID Card Registry state (5 variables)
|
||||
uint256 idCardIdentityRoot;
|
||||
uint256 idCardDscKeyRoot;
|
||||
uint256 idCardNameDobOfacRoot;
|
||||
uint256 idCardNameYobOfacRoot;
|
||||
uint256 idCardCscaRoot;
|
||||
// Aadhaar Registry state (3 variables)
|
||||
uint256 aadhaarIdentityRoot;
|
||||
uint256 aadhaarNameDobOfacRoot;
|
||||
uint256 aadhaarNameYobOfacRoot;
|
||||
}
|
||||
|
||||
PreUpgradeState preState;
|
||||
|
||||
function setUp() public {
|
||||
console2.log("================================================================================");
|
||||
console2.log("CELO MAINNET FORK TEST: Ownable -> AccessControl Upgrade");
|
||||
console2.log("================================================================================");
|
||||
|
||||
// Initialize contract references to get current owner
|
||||
hub = IdentityVerificationHubImplV2(HUB_PROXY);
|
||||
passportRegistry = IdentityRegistryImplV1(REGISTRY_PASSPORT_PROXY);
|
||||
idCardRegistry = IdentityRegistryIdCardImplV1(REGISTRY_ID_CARD_PROXY);
|
||||
aadhaarRegistry = IdentityRegistryAadhaarImplV1(REGISTRY_AADHAAR_PROXY);
|
||||
|
||||
// Get the actual current owner from the deployed contracts
|
||||
deployer = Ownable2StepUpgradeable(address(hub)).owner();
|
||||
|
||||
// Set up multisig accounts for testing role transfer
|
||||
securityMultisig = makeAddr("securityMultisig");
|
||||
operationsMultisig = makeAddr("operationsMultisig");
|
||||
|
||||
vm.deal(deployer, 100 ether);
|
||||
|
||||
console2.log("Current Owner (will execute upgrade):", deployer);
|
||||
console2.log("Critical Multisig (will receive roles):", securityMultisig);
|
||||
console2.log("Standard Multisig (will receive roles):", operationsMultisig);
|
||||
}
|
||||
|
||||
function testFullUpgradeWorkflow() public {
|
||||
console2.log("\n=== Phase 1: Capture Pre-Upgrade State (ALL Accessible State Variables) ===");
|
||||
|
||||
// Hub state (4 variables)
|
||||
preState.hubRegistryPassport = hub.registry(bytes32("e_passport"));
|
||||
preState.hubRegistryIdCard = hub.registry(bytes32("eu_id_card"));
|
||||
preState.hubRegistryAadhaar = hub.registry(bytes32("aadhaar"));
|
||||
preState.hubAadhaarWindow = hub.AADHAAR_REGISTRATION_WINDOW();
|
||||
|
||||
// Passport Registry state (6 variables)
|
||||
preState.passportIdentityRoot = passportRegistry.getIdentityCommitmentMerkleRoot();
|
||||
preState.passportDscKeyRoot = passportRegistry.getDscKeyCommitmentMerkleRoot();
|
||||
preState.passportPassportNoOfacRoot = passportRegistry.getPassportNoOfacRoot();
|
||||
preState.passportNameDobOfacRoot = passportRegistry.getNameAndDobOfacRoot();
|
||||
preState.passportNameYobOfacRoot = passportRegistry.getNameAndYobOfacRoot();
|
||||
preState.passportCscaRoot = passportRegistry.getCscaRoot();
|
||||
|
||||
// ID Card Registry state (5 variables)
|
||||
preState.idCardIdentityRoot = idCardRegistry.getIdentityCommitmentMerkleRoot();
|
||||
preState.idCardDscKeyRoot = idCardRegistry.getDscKeyCommitmentMerkleRoot();
|
||||
preState.idCardNameDobOfacRoot = idCardRegistry.getNameAndDobOfacRoot();
|
||||
preState.idCardNameYobOfacRoot = idCardRegistry.getNameAndYobOfacRoot();
|
||||
preState.idCardCscaRoot = idCardRegistry.getCscaRoot();
|
||||
|
||||
// Aadhaar Registry state (3 variables)
|
||||
preState.aadhaarIdentityRoot = aadhaarRegistry.getIdentityCommitmentMerkleRoot();
|
||||
preState.aadhaarNameDobOfacRoot = aadhaarRegistry.getNameAndDobOfacRoot();
|
||||
preState.aadhaarNameYobOfacRoot = aadhaarRegistry.getNameAndYobOfacRoot();
|
||||
|
||||
console2.log("Captured Hub state: 4 variables");
|
||||
console2.log("Captured Passport Registry state: 6 variables");
|
||||
console2.log("Captured ID Card Registry state: 5 variables");
|
||||
console2.log("Captured Aadhaar Registry state: 3 variables");
|
||||
console2.log("Total state variables captured: 18");
|
||||
|
||||
console2.log("\n=== Phase 2: Execute Upgrades ===");
|
||||
vm.startPrank(deployer);
|
||||
|
||||
// Upgrade Hub
|
||||
console2.log("Upgrading Hub...");
|
||||
Options memory hubOpts;
|
||||
// Skip ALL OpenZeppelin checks because:
|
||||
// 1. We're changing base contracts (Ownable->AccessControl) which confuses the validator
|
||||
// 2. ERC-7201 namespaced storage prevents any collision
|
||||
// 3. We COMPREHENSIVELY verify safety in this test:
|
||||
// - Phase 3: State preservation (no data loss)
|
||||
// - Phase 3.5: Library linkage (same addresses)
|
||||
// - Phase 4-6: Governance functionality (roles work correctly)
|
||||
hubOpts.unsafeSkipAllChecks = true;
|
||||
Upgrades.upgradeProxy(
|
||||
HUB_PROXY,
|
||||
"IdentityVerificationHubImplV2.sol",
|
||||
abi.encodeCall(IdentityVerificationHubImplV2.initializeGovernance, ()),
|
||||
hubOpts
|
||||
);
|
||||
|
||||
// Upgrade Passport Registry
|
||||
console2.log("Upgrading Passport Registry...");
|
||||
Options memory passportOpts;
|
||||
passportOpts.unsafeSkipAllChecks = true; // Safe: verified in test phases 3-6
|
||||
Upgrades.upgradeProxy(
|
||||
REGISTRY_PASSPORT_PROXY,
|
||||
"IdentityRegistryImplV1.sol:IdentityRegistryImplV1",
|
||||
abi.encodeCall(IdentityRegistryImplV1.initializeGovernance, ()),
|
||||
passportOpts
|
||||
);
|
||||
|
||||
// Upgrade ID Card Registry
|
||||
console2.log("Upgrading ID Card Registry...");
|
||||
Options memory idCardOpts;
|
||||
idCardOpts.unsafeSkipAllChecks = true; // Safe: verified in test phases 3-6
|
||||
Upgrades.upgradeProxy(
|
||||
REGISTRY_ID_CARD_PROXY,
|
||||
"IdentityRegistryIdCardImplV1.sol:IdentityRegistryIdCardImplV1",
|
||||
abi.encodeCall(IdentityRegistryIdCardImplV1.initializeGovernance, ()),
|
||||
idCardOpts
|
||||
);
|
||||
|
||||
// Upgrade Aadhaar Registry
|
||||
console2.log("Upgrading Aadhaar Registry...");
|
||||
Options memory aadhaarOpts;
|
||||
aadhaarOpts.unsafeSkipAllChecks = true; // Safe: verified in test phases 3-6
|
||||
Upgrades.upgradeProxy(
|
||||
REGISTRY_AADHAAR_PROXY,
|
||||
"IdentityRegistryAadhaarImplV1.sol:IdentityRegistryAadhaarImplV1",
|
||||
abi.encodeCall(IdentityRegistryAadhaarImplV1.initializeGovernance, ()),
|
||||
aadhaarOpts
|
||||
);
|
||||
|
||||
vm.stopPrank();
|
||||
console2.log("All upgrades completed");
|
||||
|
||||
console2.log("\n=== Phase 3: Verify State Preservation (ALL 18 State Variables) ===");
|
||||
|
||||
// Hub state verification (4 variables)
|
||||
assertEq(hub.registry(bytes32("e_passport")), preState.hubRegistryPassport, "Hub passport registry changed");
|
||||
assertEq(hub.registry(bytes32("eu_id_card")), preState.hubRegistryIdCard, "Hub ID card registry changed");
|
||||
assertEq(hub.registry(bytes32("aadhaar")), preState.hubRegistryAadhaar, "Hub aadhaar registry changed");
|
||||
assertEq(hub.AADHAAR_REGISTRATION_WINDOW(), preState.hubAadhaarWindow, "Hub aadhaar window changed");
|
||||
console2.log("Hub state: 4/4 variables preserved");
|
||||
|
||||
// Passport Registry state verification (6 variables)
|
||||
assertEq(
|
||||
passportRegistry.getIdentityCommitmentMerkleRoot(),
|
||||
preState.passportIdentityRoot,
|
||||
"Passport identity root changed"
|
||||
);
|
||||
assertEq(
|
||||
passportRegistry.getDscKeyCommitmentMerkleRoot(),
|
||||
preState.passportDscKeyRoot,
|
||||
"Passport DSC key root changed"
|
||||
);
|
||||
assertEq(
|
||||
passportRegistry.getPassportNoOfacRoot(),
|
||||
preState.passportPassportNoOfacRoot,
|
||||
"Passport passport# OFAC root changed"
|
||||
);
|
||||
assertEq(
|
||||
passportRegistry.getNameAndDobOfacRoot(),
|
||||
preState.passportNameDobOfacRoot,
|
||||
"Passport name+DOB OFAC root changed"
|
||||
);
|
||||
assertEq(
|
||||
passportRegistry.getNameAndYobOfacRoot(),
|
||||
preState.passportNameYobOfacRoot,
|
||||
"Passport name+YOB OFAC root changed"
|
||||
);
|
||||
assertEq(passportRegistry.getCscaRoot(), preState.passportCscaRoot, "Passport CSCA root changed");
|
||||
console2.log("Passport Registry: 6/6 variables preserved");
|
||||
|
||||
// ID Card Registry state verification (5 variables)
|
||||
assertEq(
|
||||
idCardRegistry.getIdentityCommitmentMerkleRoot(),
|
||||
preState.idCardIdentityRoot,
|
||||
"ID Card identity root changed"
|
||||
);
|
||||
assertEq(
|
||||
idCardRegistry.getDscKeyCommitmentMerkleRoot(),
|
||||
preState.idCardDscKeyRoot,
|
||||
"ID Card DSC key root changed"
|
||||
);
|
||||
assertEq(
|
||||
idCardRegistry.getNameAndDobOfacRoot(),
|
||||
preState.idCardNameDobOfacRoot,
|
||||
"ID Card name+DOB OFAC root changed"
|
||||
);
|
||||
assertEq(
|
||||
idCardRegistry.getNameAndYobOfacRoot(),
|
||||
preState.idCardNameYobOfacRoot,
|
||||
"ID Card name+YOB OFAC root changed"
|
||||
);
|
||||
assertEq(idCardRegistry.getCscaRoot(), preState.idCardCscaRoot, "ID Card CSCA root changed");
|
||||
console2.log("ID Card Registry: 5/5 variables preserved");
|
||||
|
||||
// Aadhaar Registry state verification (3 variables)
|
||||
assertEq(
|
||||
aadhaarRegistry.getIdentityCommitmentMerkleRoot(),
|
||||
preState.aadhaarIdentityRoot,
|
||||
"Aadhaar identity root changed"
|
||||
);
|
||||
assertEq(
|
||||
aadhaarRegistry.getNameAndDobOfacRoot(),
|
||||
preState.aadhaarNameDobOfacRoot,
|
||||
"Aadhaar name+DOB OFAC root changed"
|
||||
);
|
||||
assertEq(
|
||||
aadhaarRegistry.getNameAndYobOfacRoot(),
|
||||
preState.aadhaarNameYobOfacRoot,
|
||||
"Aadhaar name+YOB OFAC root changed"
|
||||
);
|
||||
console2.log("Aadhaar Registry: 3/3 variables preserved");
|
||||
|
||||
console2.log("TOTAL: 18/18 state variables VERIFIED - NO storage collisions!");
|
||||
|
||||
console2.log("\n=== Phase 4: Verify Governance Roles ===");
|
||||
|
||||
// Deployer should have both roles initially
|
||||
assertTrue(hub.hasRole(SECURITY_ROLE, deployer), "Deployer missing SECURITY_ROLE on Hub");
|
||||
assertTrue(hub.hasRole(OPERATIONS_ROLE, deployer), "Deployer missing OPERATIONS_ROLE on Hub");
|
||||
assertTrue(passportRegistry.hasRole(SECURITY_ROLE, deployer), "Deployer missing SECURITY_ROLE on Passport");
|
||||
assertTrue(passportRegistry.hasRole(OPERATIONS_ROLE, deployer), "Deployer missing OPERATIONS_ROLE on Passport");
|
||||
assertTrue(idCardRegistry.hasRole(SECURITY_ROLE, deployer), "Deployer missing SECURITY_ROLE on ID Card");
|
||||
assertTrue(idCardRegistry.hasRole(OPERATIONS_ROLE, deployer), "Deployer missing OPERATIONS_ROLE on ID Card");
|
||||
|
||||
console2.log("Deployer has all required roles");
|
||||
|
||||
console2.log("\n=== Phase 5: Transfer Roles to Multisigs ===");
|
||||
|
||||
vm.startPrank(deployer);
|
||||
|
||||
// Grant roles to multisigs
|
||||
hub.grantRole(SECURITY_ROLE, securityMultisig);
|
||||
hub.grantRole(OPERATIONS_ROLE, operationsMultisig);
|
||||
passportRegistry.grantRole(SECURITY_ROLE, securityMultisig);
|
||||
passportRegistry.grantRole(OPERATIONS_ROLE, operationsMultisig);
|
||||
idCardRegistry.grantRole(SECURITY_ROLE, securityMultisig);
|
||||
idCardRegistry.grantRole(OPERATIONS_ROLE, operationsMultisig);
|
||||
|
||||
// Deployer renounces roles
|
||||
hub.renounceRole(SECURITY_ROLE, deployer);
|
||||
hub.renounceRole(OPERATIONS_ROLE, deployer);
|
||||
passportRegistry.renounceRole(SECURITY_ROLE, deployer);
|
||||
passportRegistry.renounceRole(OPERATIONS_ROLE, deployer);
|
||||
idCardRegistry.renounceRole(SECURITY_ROLE, deployer);
|
||||
idCardRegistry.renounceRole(OPERATIONS_ROLE, deployer);
|
||||
|
||||
vm.stopPrank();
|
||||
|
||||
console2.log("Roles transferred to multisigs");
|
||||
|
||||
console2.log("\n=== Phase 6: Verify Final State ===");
|
||||
|
||||
// Deployer should have NO roles
|
||||
assertFalse(hub.hasRole(SECURITY_ROLE, deployer), "Deployer still has SECURITY_ROLE on Hub");
|
||||
assertFalse(hub.hasRole(OPERATIONS_ROLE, deployer), "Deployer still has OPERATIONS_ROLE on Hub");
|
||||
|
||||
// Multisigs should have roles
|
||||
assertTrue(hub.hasRole(SECURITY_ROLE, securityMultisig), "Critical multisig missing SECURITY_ROLE on Hub");
|
||||
assertTrue(
|
||||
hub.hasRole(OPERATIONS_ROLE, operationsMultisig),
|
||||
"Standard multisig missing OPERATIONS_ROLE on Hub"
|
||||
);
|
||||
assertTrue(
|
||||
passportRegistry.hasRole(SECURITY_ROLE, securityMultisig),
|
||||
"Critical multisig missing SECURITY_ROLE on Passport"
|
||||
);
|
||||
assertTrue(
|
||||
passportRegistry.hasRole(OPERATIONS_ROLE, operationsMultisig),
|
||||
"Standard multisig missing OPERATIONS_ROLE on Passport"
|
||||
);
|
||||
assertTrue(
|
||||
idCardRegistry.hasRole(SECURITY_ROLE, securityMultisig),
|
||||
"Critical multisig missing SECURITY_ROLE on ID Card"
|
||||
);
|
||||
assertTrue(
|
||||
idCardRegistry.hasRole(OPERATIONS_ROLE, operationsMultisig),
|
||||
"Standard multisig missing OPERATIONS_ROLE on ID Card"
|
||||
);
|
||||
|
||||
console2.log("Multisigs have full control");
|
||||
console2.log("Deployer has ZERO control");
|
||||
|
||||
console2.log("\n================================================================================");
|
||||
console2.log("UPGRADE TEST PASSED - Safe to execute on mainnet");
|
||||
console2.log("================================================================================");
|
||||
}
|
||||
}
|
||||
@@ -2,27 +2,55 @@ import { expect } from "chai";
|
||||
import { BigNumberish, TransactionReceipt } from "ethers";
|
||||
import { ethers } from "hardhat";
|
||||
import { poseidon2 } from "poseidon-lite";
|
||||
import { createHash } from "crypto";
|
||||
import { CIRCUIT_CONSTANTS, DscVerifierId, RegisterVerifierId } from "@selfxyz/common/constants/constants";
|
||||
import { formatCountriesList, reverseBytes } from "@selfxyz/common/utils/circuits/formatInputs";
|
||||
import { castFromScope } from "@selfxyz/common/utils/circuits/uuid";
|
||||
import { ATTESTATION_ID } from "../utils/constants";
|
||||
import { deploySystemFixtures } from "../utils/deployment";
|
||||
import { deploySystemFixturesV2 } from "../utils/deploymentV2";
|
||||
import BalanceTree from "../utils/example/balance-tree";
|
||||
import { Formatter } from "../utils/formatter";
|
||||
import { generateDscProof, generateRegisterProof, generateVcAndDiscloseProof } from "../utils/generateProof";
|
||||
import { LeanIMT } from "@openpassport/zk-kit-lean-imt";
|
||||
import serialized_dsc_tree from "../../../common/pubkeys/serialized_dsc_tree.json";
|
||||
import { DeployedActors, VcAndDiscloseHubProof } from "../utils/types";
|
||||
import { DeployedActorsV2 } from "../utils/types";
|
||||
import { generateRandomFieldElement, splitHexFromBack } from "../utils/utils";
|
||||
|
||||
// Helper function to calculate user identifier hash
|
||||
function calculateUserIdentifierHash(userContextData: string): string {
|
||||
const sha256Hash = createHash("sha256")
|
||||
.update(Buffer.from(userContextData.slice(2), "hex"))
|
||||
.digest();
|
||||
const ripemdHash = createHash("ripemd160").update(sha256Hash).digest();
|
||||
return "0x" + ripemdHash.toString("hex").padStart(40, "0");
|
||||
}
|
||||
|
||||
// Helper function to create V2 proof data format for verifySelfProof
|
||||
function createV2ProofData(proof: any, userAddress: string, userData: string = "airdrop-user-data") {
|
||||
const destChainId = ethers.zeroPadValue(ethers.toBeHex(31337), 32);
|
||||
const userContextData = ethers.solidityPacked(
|
||||
["bytes32", "bytes32", "bytes"],
|
||||
[destChainId, ethers.zeroPadValue(userAddress, 32), ethers.toUtf8Bytes(userData)],
|
||||
);
|
||||
|
||||
const attestationId = ethers.zeroPadValue(ethers.toBeHex(BigInt(ATTESTATION_ID.E_PASSPORT)), 32);
|
||||
const encodedProof = ethers.AbiCoder.defaultAbiCoder().encode(
|
||||
["tuple(uint256[2] a, uint256[2][2] b, uint256[2] c, uint256[] pubSignals)"],
|
||||
[[proof.a, proof.b, proof.c, proof.pubSignals]],
|
||||
);
|
||||
|
||||
const proofData = ethers.solidityPacked(["bytes32", "bytes"], [attestationId, encodedProof]);
|
||||
|
||||
return { proofData, userContextData };
|
||||
}
|
||||
|
||||
describe("End to End Tests", function () {
|
||||
this.timeout(0);
|
||||
|
||||
let deployedActors: DeployedActors;
|
||||
let deployedActors: DeployedActorsV2;
|
||||
let snapshotId: string;
|
||||
|
||||
before(async () => {
|
||||
deployedActors = await deploySystemFixtures();
|
||||
deployedActors = await deploySystemFixturesV2();
|
||||
snapshotId = await ethers.provider.send("evm_snapshot", []);
|
||||
});
|
||||
|
||||
@@ -32,7 +60,10 @@ describe("End to End Tests", function () {
|
||||
});
|
||||
|
||||
it("register dsc key commitment, register identity commitment, verify commitment and disclose attrs and claim airdrop", async () => {
|
||||
const { hub, registry, mockPassport, owner, user1 } = deployedActors;
|
||||
const { hub, registry, mockPassport, owner, user1, testSelfVerificationRoot, poseidonT3 } = deployedActors;
|
||||
|
||||
// V2 hub requires attestationId as bytes32
|
||||
const attestationIdBytes32 = ethers.zeroPadValue(ethers.toBeHex(BigInt(ATTESTATION_ID.E_PASSPORT)), 32);
|
||||
|
||||
// register dsc key
|
||||
// To increase test performance, we will just set one dsc key with groth16 proof
|
||||
@@ -45,7 +76,11 @@ describe("End to End Tests", function () {
|
||||
if (BigInt(dscKeys[0][i]) == dscProof.pubSignals[CIRCUIT_CONSTANTS.DSC_TREE_LEAF_INDEX]) {
|
||||
const previousRoot = await registry.getDscKeyCommitmentMerkleRoot();
|
||||
const previousSize = await registry.getDscKeyCommitmentTreeSize();
|
||||
registerDscTx = await hub.registerDscKeyCommitment(DscVerifierId.dsc_sha256_rsa_65537_4096, dscProof);
|
||||
registerDscTx = await hub.registerDscKeyCommitment(
|
||||
attestationIdBytes32,
|
||||
DscVerifierId.dsc_sha256_rsa_65537_4096,
|
||||
dscProof,
|
||||
);
|
||||
const receipt = (await registerDscTx.wait()) as TransactionReceipt;
|
||||
const event = receipt?.logs.find(
|
||||
(log) => log.topics[0] === registry.interface.getEvent("DscKeyCommitmentRegistered").topicHash,
|
||||
@@ -90,7 +125,8 @@ describe("End to End Tests", function () {
|
||||
const imt = new LeanIMT<bigint>(hashFunction);
|
||||
await imt.insert(BigInt(registerProof.pubSignals[CIRCUIT_CONSTANTS.REGISTER_COMMITMENT_INDEX]));
|
||||
|
||||
const tx = await hub.registerPassportCommitment(
|
||||
const tx = await hub.registerCommitment(
|
||||
attestationIdBytes32,
|
||||
RegisterVerifierId.register_sha256_sha256_sha256_rsa_65537_4096,
|
||||
registerProof,
|
||||
);
|
||||
@@ -104,7 +140,7 @@ describe("End to End Tests", function () {
|
||||
registerProof.pubSignals[CIRCUIT_CONSTANTS.REGISTER_COMMITMENT_INDEX],
|
||||
);
|
||||
const identityNullifier = await registry.nullifiers(
|
||||
ATTESTATION_ID.E_PASSPORT,
|
||||
attestationIdBytes32,
|
||||
registerProof.pubSignals[CIRCUIT_CONSTANTS.REGISTER_NULLIFIER_INDEX],
|
||||
);
|
||||
|
||||
@@ -134,11 +170,25 @@ describe("End to End Tests", function () {
|
||||
reverseBytes(Formatter.bytesToHexString(new Uint8Array(formatCountriesList(forbiddenCountriesList)))),
|
||||
);
|
||||
|
||||
// Get the scope from testSelfVerificationRoot
|
||||
const testRootScope = await testSelfVerificationRoot.scope();
|
||||
|
||||
// Calculate user identifier hash for verification
|
||||
const destChainId = ethers.zeroPadValue(ethers.toBeHex(31337), 32);
|
||||
const user1Address = await user1.getAddress();
|
||||
const userData = ethers.toUtf8Bytes("test-user-data");
|
||||
const tempUserContextData = ethers.solidityPacked(
|
||||
["bytes32", "bytes32", "bytes"],
|
||||
[destChainId, ethers.zeroPadValue(user1Address, 32), userData],
|
||||
);
|
||||
const userIdentifierHash = calculateUserIdentifierHash(tempUserContextData);
|
||||
|
||||
// Generate proof for V2 verification
|
||||
const vcAndDiscloseProof = await generateVcAndDiscloseProof(
|
||||
registerSecret,
|
||||
BigInt(ATTESTATION_ID.E_PASSPORT).toString(),
|
||||
mockPassport,
|
||||
"test-scope",
|
||||
testRootScope.toString(),
|
||||
new Array(88).fill("1"),
|
||||
"1",
|
||||
imt,
|
||||
@@ -148,59 +198,114 @@ describe("End to End Tests", function () {
|
||||
undefined,
|
||||
undefined,
|
||||
forbiddenCountriesList,
|
||||
(await user1.getAddress()).slice(2),
|
||||
userIdentifierHash,
|
||||
);
|
||||
|
||||
const vcAndDiscloseHubProof: VcAndDiscloseHubProof = {
|
||||
// Set up verification config for testSelfVerificationRoot
|
||||
const verificationConfigV2 = {
|
||||
olderThanEnabled: true,
|
||||
olderThan: "20",
|
||||
forbiddenCountriesEnabled: true,
|
||||
forbiddenCountriesListPacked: countriesListPacked,
|
||||
forbiddenCountriesListPacked: countriesListPacked as [BigNumberish, BigNumberish, BigNumberish, BigNumberish],
|
||||
ofacEnabled: [true, true, true] as [boolean, boolean, boolean],
|
||||
vcAndDiscloseProof: vcAndDiscloseProof,
|
||||
};
|
||||
|
||||
const result = await hub.verifyVcAndDisclose(vcAndDiscloseHubProof);
|
||||
await testSelfVerificationRoot.setVerificationConfig(verificationConfigV2);
|
||||
|
||||
expect(result.identityCommitmentRoot).to.equal(
|
||||
vcAndDiscloseProof.pubSignals[CIRCUIT_CONSTANTS.VC_AND_DISCLOSE_MERKLE_ROOT_INDEX],
|
||||
// Create V2 proof format and verify via testSelfVerificationRoot
|
||||
const { proofData, userContextData: verifyUserContextData } = createV2ProofData(
|
||||
vcAndDiscloseProof,
|
||||
user1Address,
|
||||
"test-user-data",
|
||||
);
|
||||
expect(result.revealedDataPacked).to.have.lengthOf(3);
|
||||
expect(result.nullifier).to.equal(vcAndDiscloseProof.pubSignals[CIRCUIT_CONSTANTS.VC_AND_DISCLOSE_NULLIFIER_INDEX]);
|
||||
expect(result.attestationId).to.equal(
|
||||
vcAndDiscloseProof.pubSignals[CIRCUIT_CONSTANTS.VC_AND_DISCLOSE_ATTESTATION_ID_INDEX],
|
||||
|
||||
// Reset test state before verification
|
||||
await testSelfVerificationRoot.resetTestState();
|
||||
|
||||
// Verify the proof through V2 architecture
|
||||
await testSelfVerificationRoot.connect(user1).verifySelfProof(proofData, verifyUserContextData);
|
||||
|
||||
// Check verification was successful
|
||||
expect(await testSelfVerificationRoot.verificationSuccessful()).to.equal(true);
|
||||
|
||||
// Get the verification output and verify it
|
||||
const lastOutput = await testSelfVerificationRoot.lastOutput();
|
||||
expect(lastOutput).to.not.equal("0x");
|
||||
|
||||
// Verify attestationId matches both the expected bytes32 and the proof pubSignals
|
||||
expect(lastOutput.attestationId).to.equal(attestationIdBytes32);
|
||||
expect(lastOutput.attestationId).to.equal(
|
||||
ethers.zeroPadValue(
|
||||
ethers.toBeHex(vcAndDiscloseProof.pubSignals[CIRCUIT_CONSTANTS.VC_AND_DISCLOSE_ATTESTATION_ID_INDEX]),
|
||||
32,
|
||||
),
|
||||
);
|
||||
expect(result.userIdentifier).to.equal(
|
||||
vcAndDiscloseProof.pubSignals[CIRCUIT_CONSTANTS.VC_AND_DISCLOSE_USER_IDENTIFIER_INDEX],
|
||||
|
||||
// Verify nullifier matches the proof pubSignals
|
||||
expect(lastOutput.nullifier).to.equal(
|
||||
vcAndDiscloseProof.pubSignals[CIRCUIT_CONSTANTS.VC_AND_DISCLOSE_NULLIFIER_INDEX],
|
||||
);
|
||||
expect(result.scope).to.equal(vcAndDiscloseProof.pubSignals[CIRCUIT_CONSTANTS.VC_AND_DISCLOSE_SCOPE_INDEX]);
|
||||
for (let i = 0; i < 4; i++) {
|
||||
expect(result.forbiddenCountriesListPacked[i]).to.equal(BigInt(countriesListPacked[i]));
|
||||
}
|
||||
|
||||
// Verify userIdentifier is set
|
||||
expect(lastOutput.userIdentifier).to.not.equal(0n);
|
||||
|
||||
// Verify olderThan value
|
||||
expect(lastOutput.olderThan).to.equal(20n);
|
||||
|
||||
const tokenFactory = await ethers.getContractFactory("AirdropToken");
|
||||
const token = await tokenFactory.connect(owner).deploy();
|
||||
await token.waitForDeployment();
|
||||
|
||||
const airdropFactory = await ethers.getContractFactory("Airdrop");
|
||||
const airdrop = await airdropFactory.connect(owner).deploy(
|
||||
hub.target,
|
||||
castFromScope("test-scope"),
|
||||
ATTESTATION_ID.E_PASSPORT,
|
||||
token.target,
|
||||
true,
|
||||
20,
|
||||
// @ts-expect-error
|
||||
true,
|
||||
countriesListPacked as [BigNumberish, BigNumberish, BigNumberish, BigNumberish],
|
||||
[true, true, true],
|
||||
);
|
||||
const airdrop = await airdropFactory.connect(owner).deploy(hub.target, "test-scope", token.target);
|
||||
await airdrop.waitForDeployment();
|
||||
|
||||
// Set up verification config for the airdrop
|
||||
const configTx = await hub.connect(owner).setVerificationConfigV2(verificationConfigV2);
|
||||
const configReceipt = await configTx.wait();
|
||||
const configId = configReceipt!.logs[0].topics[1];
|
||||
|
||||
// Set the config ID in the airdrop contract
|
||||
await airdrop.connect(owner).setConfigId(configId);
|
||||
|
||||
await token.connect(owner).mint(airdrop.target, BigInt(1000000000000000000));
|
||||
|
||||
// Generate proof with the airdrop's actual scope
|
||||
const airdropScope = await airdrop.scope();
|
||||
|
||||
// Calculate the user identifier hash for the airdrop proof
|
||||
const airdropUserData = ethers.toUtf8Bytes("airdrop-user-data");
|
||||
const airdropTempUserContextData = ethers.solidityPacked(
|
||||
["bytes32", "bytes32", "bytes"],
|
||||
[destChainId, ethers.zeroPadValue(user1Address, 32), airdropUserData],
|
||||
);
|
||||
const airdropUserIdentifierHash = calculateUserIdentifierHash(airdropTempUserContextData);
|
||||
|
||||
const airdropVcAndDiscloseProof = await generateVcAndDiscloseProof(
|
||||
registerSecret,
|
||||
BigInt(ATTESTATION_ID.E_PASSPORT).toString(),
|
||||
mockPassport,
|
||||
airdropScope.toString(),
|
||||
new Array(88).fill("1"),
|
||||
"1",
|
||||
imt,
|
||||
"20",
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
forbiddenCountriesList,
|
||||
airdropUserIdentifierHash,
|
||||
);
|
||||
|
||||
await airdrop.connect(owner).openRegistration();
|
||||
await airdrop.connect(user1).verifySelfProof(vcAndDiscloseProof);
|
||||
|
||||
// Create V2 proof format for verifySelfProof
|
||||
const { proofData: airdropProofData, userContextData: airdropUserContextData } = createV2ProofData(
|
||||
airdropVcAndDiscloseProof,
|
||||
await user1.getAddress(),
|
||||
);
|
||||
await airdrop.connect(user1).verifySelfProof(airdropProofData, airdropUserContextData);
|
||||
await airdrop.connect(owner).closeRegistration();
|
||||
|
||||
const tree = new BalanceTree([{ account: await user1.getAddress(), amount: BigInt(1000000000000000000) }]);
|
||||
@@ -228,19 +333,13 @@ describe("End to End Tests", function () {
|
||||
const isClaimed = await airdrop.claimed(await user1.getAddress());
|
||||
expect(isClaimed).to.be.true;
|
||||
|
||||
const readableData = await hub.getReadableRevealedData(
|
||||
[result.revealedDataPacked[0], result.revealedDataPacked[1], result.revealedDataPacked[2]],
|
||||
["0", "1", "2", "3", "4", "5", "6", "7", "8"],
|
||||
);
|
||||
|
||||
expect(readableData[0]).to.equal("FRA");
|
||||
expect(readableData[1]).to.deep.equal(["ALPHONSE HUGHUES ALBERT", "DUPONT"]);
|
||||
expect(readableData[2]).to.equal("15AA81234");
|
||||
expect(readableData[3]).to.equal("FRA");
|
||||
expect(readableData[4]).to.equal("31-01-94");
|
||||
expect(readableData[5]).to.equal("M");
|
||||
expect(readableData[6]).to.equal("31-10-40");
|
||||
expect(readableData[7]).to.equal(20n);
|
||||
expect(readableData[8]).to.equal(1n);
|
||||
// Verify disclosed attributes from lastOutput
|
||||
expect(lastOutput.issuingState).to.equal("FRA");
|
||||
expect(lastOutput.idNumber).to.equal("15AA81234");
|
||||
expect(lastOutput.nationality).to.equal("FRA");
|
||||
expect(lastOutput.dateOfBirth).to.equal("31-01-94");
|
||||
expect(lastOutput.gender).to.equal("M");
|
||||
expect(lastOutput.expiryDate).to.equal("31-10-40");
|
||||
expect(lastOutput.olderThan).to.equal(20n);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,8 +11,9 @@ import { BigNumberish } from "ethers";
|
||||
import { generateRandomFieldElement, getStartOfDayTimestamp, splitHexFromBack } from "../utils/utils";
|
||||
import { Formatter, CircuitAttributeHandler } from "../utils/formatter";
|
||||
import { formatCountriesList, reverseBytes, reverseCountryBytes } from "@selfxyz/common/utils/circuits/formatInputs";
|
||||
import { getPackedForbiddenCountries } from "@selfxyz/common/utils/sanctions";
|
||||
import { getPackedForbiddenCountries } from "@selfxyz/common/utils/contracts/forbiddenCountries";
|
||||
import { countries, Country3LetterCode } from "@selfxyz/common/constants/countries";
|
||||
import { castFromScope } from "@selfxyz/common/utils/circuits/uuid";
|
||||
import path from "path";
|
||||
|
||||
describe("VC and Disclose", () => {
|
||||
@@ -100,7 +101,7 @@ describe("VC and Disclose", () => {
|
||||
registerSecret,
|
||||
BigInt(ATTESTATION_ID.E_PASSPORT).toString(),
|
||||
deployedActors.mockPassport,
|
||||
"test-scope",
|
||||
castFromScope("test-scope"),
|
||||
new Array(88).fill("1"),
|
||||
"1",
|
||||
imt,
|
||||
@@ -110,7 +111,7 @@ describe("VC and Disclose", () => {
|
||||
undefined,
|
||||
undefined,
|
||||
forbiddenCountriesList,
|
||||
(await deployedActors.user1.getAddress()).slice(2),
|
||||
await deployedActors.user1.getAddress(),
|
||||
);
|
||||
snapshotId = await ethers.provider.send("evm_snapshot", []);
|
||||
});
|
||||
@@ -439,6 +440,7 @@ describe("VC and Disclose", () => {
|
||||
const { hub, registry, owner, mockPassport } = deployedActors;
|
||||
|
||||
const hashFunction = (a: bigint, b: bigint) => poseidon2([a, b]);
|
||||
const LeanIMT = await import("@openpassport/zk-kit-lean-imt").then((mod) => mod.LeanIMT);
|
||||
const imt = new LeanIMT<bigint>(hashFunction);
|
||||
imt.insert(BigInt(commitment));
|
||||
|
||||
@@ -448,7 +450,7 @@ describe("VC and Disclose", () => {
|
||||
registerSecret,
|
||||
BigInt(ATTESTATION_ID.E_PASSPORT).toString(),
|
||||
mockPassport,
|
||||
"test-scope",
|
||||
castFromScope("test-scope"),
|
||||
new Array(88).fill("1"),
|
||||
"1",
|
||||
imt,
|
||||
@@ -746,43 +748,60 @@ describe("VC and Disclose", () => {
|
||||
it("should parse forbidden countries with CircuitAttributeHandler", async () => {
|
||||
const { hub } = deployedActors;
|
||||
|
||||
const forbiddenCountriesListPacked = splitHexFromBack(
|
||||
reverseCountryBytes(Formatter.bytesToHexString(new Uint8Array(formatCountriesList(forbiddenCountriesList)))),
|
||||
);
|
||||
const localForbiddenCountriesList = ["AFG", "ABC", "CBA"] as const;
|
||||
const forbiddenCountriesListPacked = getPackedForbiddenCountries([...localForbiddenCountriesList]);
|
||||
const readableForbiddenCountries = await hub.getReadableForbiddenCountries(forbiddenCountriesListPacked);
|
||||
|
||||
expect(readableForbiddenCountries[0]).to.equal(forbiddenCountriesList[0]);
|
||||
expect(readableForbiddenCountries[1]).to.equal(forbiddenCountriesList[1]);
|
||||
expect(readableForbiddenCountries[2]).to.equal(forbiddenCountriesList[2]);
|
||||
expect(readableForbiddenCountries[0]).to.equal(localForbiddenCountriesList[0]);
|
||||
expect(readableForbiddenCountries[1]).to.equal(localForbiddenCountriesList[1]);
|
||||
expect(readableForbiddenCountries[2]).to.equal(localForbiddenCountriesList[2]);
|
||||
});
|
||||
|
||||
it("should return maximum length of forbidden countries", async () => {
|
||||
const { hub } = deployedActors;
|
||||
|
||||
const forbiddenCountriesList = ["AAA", "FRA", "CBA", "CBA", "CBA", "CBA", "CBA", "CBA", "CBA", "CBA"];
|
||||
const forbiddenCountriesListPacked = splitHexFromBack(
|
||||
reverseCountryBytes(Formatter.bytesToHexString(new Uint8Array(formatCountriesList(forbiddenCountriesList)))),
|
||||
);
|
||||
const localForbiddenCountriesList = [
|
||||
"AAA",
|
||||
"FRA",
|
||||
"CBA",
|
||||
"CBA",
|
||||
"CBA",
|
||||
"CBA",
|
||||
"CBA",
|
||||
"CBA",
|
||||
"CBA",
|
||||
"CBA",
|
||||
] as const;
|
||||
const forbiddenCountriesListPacked = getPackedForbiddenCountries([...localForbiddenCountriesList]);
|
||||
const readableForbiddenCountries = await hub.getReadableForbiddenCountries(forbiddenCountriesListPacked);
|
||||
expect(readableForbiddenCountries.length).to.equal(40);
|
||||
expect(readableForbiddenCountries[0]).to.equal(forbiddenCountriesList[0]);
|
||||
expect(readableForbiddenCountries[1]).to.equal(forbiddenCountriesList[1]);
|
||||
expect(readableForbiddenCountries[2]).to.equal(forbiddenCountriesList[2]);
|
||||
expect(readableForbiddenCountries[3]).to.equal(forbiddenCountriesList[3]);
|
||||
expect(readableForbiddenCountries[4]).to.equal(forbiddenCountriesList[4]);
|
||||
expect(readableForbiddenCountries[5]).to.equal(forbiddenCountriesList[5]);
|
||||
expect(readableForbiddenCountries[6]).to.equal(forbiddenCountriesList[6]);
|
||||
expect(readableForbiddenCountries[7]).to.equal(forbiddenCountriesList[7]);
|
||||
expect(readableForbiddenCountries[8]).to.equal(forbiddenCountriesList[8]);
|
||||
expect(readableForbiddenCountries[9]).to.equal(forbiddenCountriesList[9]);
|
||||
expect(readableForbiddenCountries[0]).to.equal(localForbiddenCountriesList[0]);
|
||||
expect(readableForbiddenCountries[1]).to.equal(localForbiddenCountriesList[1]);
|
||||
expect(readableForbiddenCountries[2]).to.equal(localForbiddenCountriesList[2]);
|
||||
expect(readableForbiddenCountries[3]).to.equal(localForbiddenCountriesList[3]);
|
||||
expect(readableForbiddenCountries[4]).to.equal(localForbiddenCountriesList[4]);
|
||||
expect(readableForbiddenCountries[5]).to.equal(localForbiddenCountriesList[5]);
|
||||
expect(readableForbiddenCountries[6]).to.equal(localForbiddenCountriesList[6]);
|
||||
expect(readableForbiddenCountries[7]).to.equal(localForbiddenCountriesList[7]);
|
||||
expect(readableForbiddenCountries[8]).to.equal(localForbiddenCountriesList[8]);
|
||||
expect(readableForbiddenCountries[9]).to.equal(localForbiddenCountriesList[9]);
|
||||
});
|
||||
|
||||
it("should fail when getReadableForbiddenCountries is called by non-proxy", async () => {
|
||||
const { hubImpl } = deployedActors;
|
||||
const forbiddenCountriesList = ["AAA", "FRA", "CBA", "CBA", "CBA", "CBA", "CBA", "CBA", "CBA", "CBA"];
|
||||
const forbiddenCountriesListPacked = splitHexFromBack(
|
||||
reverseCountryBytes(Formatter.bytesToHexString(new Uint8Array(formatCountriesList(forbiddenCountriesList)))),
|
||||
);
|
||||
const localForbiddenCountriesList = [
|
||||
"AAA",
|
||||
"FRA",
|
||||
"CBA",
|
||||
"CBA",
|
||||
"CBA",
|
||||
"CBA",
|
||||
"CBA",
|
||||
"CBA",
|
||||
"CBA",
|
||||
"CBA",
|
||||
] as const;
|
||||
const forbiddenCountriesListPacked = getPackedForbiddenCountries([...localForbiddenCountriesList]);
|
||||
await expect(hubImpl.getReadableForbiddenCountries(forbiddenCountriesListPacked)).to.be.revertedWithCustomError(
|
||||
hubImpl,
|
||||
"UUPSUnauthorizedCallContext",
|
||||
|
||||
@@ -10,6 +10,7 @@ import { poseidon2 } from "poseidon-lite";
|
||||
import { generateVcAndDiscloseProof, parseSolidityCalldata } from "../utils/generateProof";
|
||||
import { Formatter } from "../utils/formatter";
|
||||
import { formatCountriesList, reverseBytes } from "@selfxyz/common/utils/circuits/formatInputs";
|
||||
import { stringToBigInt } from "@selfxyz/common/utils/scope";
|
||||
import { VerifyAll } from "../../typechain-types";
|
||||
import { getSMTs } from "../utils/generateProof";
|
||||
import { Groth16Proof, PublicSignals, groth16 } from "snarkjs";
|
||||
@@ -102,7 +103,7 @@ describe("VerifyAll", () => {
|
||||
registerSecret,
|
||||
BigInt(ATTESTATION_ID.E_PASSPORT).toString(),
|
||||
deployedActors.mockPassport,
|
||||
"test-scope",
|
||||
stringToBigInt("test-scope").toString(),
|
||||
new Array(88).fill("1"),
|
||||
"1",
|
||||
imt,
|
||||
@@ -112,7 +113,7 @@ describe("VerifyAll", () => {
|
||||
undefined,
|
||||
undefined,
|
||||
forbiddenCountriesList,
|
||||
(await deployedActors.user1.getAddress()).slice(2),
|
||||
await deployedActors.user1.getAddress(),
|
||||
);
|
||||
snapshotId = await ethers.provider.send("evm_snapshot", []);
|
||||
});
|
||||
@@ -293,7 +294,7 @@ describe("VerifyAll", () => {
|
||||
registerSecret,
|
||||
BigInt(ATTESTATION_ID.E_PASSPORT).toString(),
|
||||
deployedActors.mockPassport,
|
||||
"test-scope",
|
||||
stringToBigInt("test-scope").toString(),
|
||||
new Array(88).fill("1"),
|
||||
"1",
|
||||
imt,
|
||||
@@ -460,7 +461,7 @@ describe("VerifyAll", () => {
|
||||
const newHubAddress = await deployedActors.user1.getAddress();
|
||||
await expect(verifyAll.connect(deployedActors.user1).setHub(newHubAddress)).to.be.revertedWithCustomError(
|
||||
verifyAll,
|
||||
"OwnableUnauthorizedAccount",
|
||||
"AccessControlUnauthorizedAccount",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -468,7 +469,7 @@ describe("VerifyAll", () => {
|
||||
const newRegistryAddress = await deployedActors.user1.getAddress();
|
||||
await expect(
|
||||
verifyAll.connect(deployedActors.user1).setRegistry(newRegistryAddress),
|
||||
).to.be.revertedWithCustomError(verifyAll, "OwnableUnauthorizedAccount");
|
||||
).to.be.revertedWithCustomError(verifyAll, "AccessControlUnauthorizedAccount");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -357,7 +357,7 @@ describe("Unit Tests for IdentityRegistry", () => {
|
||||
|
||||
await expect(registry.connect(user1).updateHub(newHubAddress)).to.be.revertedWithCustomError(
|
||||
registry,
|
||||
"OwnableUnauthorizedAccount",
|
||||
"AccessControlUnauthorizedAccount",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -394,7 +394,7 @@ describe("Unit Tests for IdentityRegistry", () => {
|
||||
expect(await registry.getNameAndYobOfacRoot()).to.equal(yobRoot);
|
||||
});
|
||||
|
||||
it("should not update OFAC root if caller is not owner", async () => {
|
||||
it("should not update OFAC root if caller does not have OPERATIONS_ROLE", async () => {
|
||||
const { registry, user1 } = deployedActors;
|
||||
const passportRoot = generateRandomFieldElement();
|
||||
const dobRoot = generateRandomFieldElement();
|
||||
@@ -402,15 +402,15 @@ describe("Unit Tests for IdentityRegistry", () => {
|
||||
|
||||
await expect(registry.connect(user1).updatePassportNoOfacRoot(passportRoot)).to.be.revertedWithCustomError(
|
||||
registry,
|
||||
"OwnableUnauthorizedAccount",
|
||||
"AccessControlUnauthorizedAccount",
|
||||
);
|
||||
await expect(registry.connect(user1).updateNameAndDobOfacRoot(dobRoot)).to.be.revertedWithCustomError(
|
||||
registry,
|
||||
"OwnableUnauthorizedAccount",
|
||||
"AccessControlUnauthorizedAccount",
|
||||
);
|
||||
await expect(registry.connect(user1).updateNameAndYobOfacRoot(yobRoot)).to.be.revertedWithCustomError(
|
||||
registry,
|
||||
"OwnableUnauthorizedAccount",
|
||||
"AccessControlUnauthorizedAccount",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -443,13 +443,13 @@ describe("Unit Tests for IdentityRegistry", () => {
|
||||
expect(await registry.getCscaRoot()).to.equal(newCscaRoot);
|
||||
});
|
||||
|
||||
it("should not update CSCA root if caller is not owner", async () => {
|
||||
it("should not update CSCA root if caller does not have OPERATIONS_ROLE", async () => {
|
||||
const { registry, user1 } = deployedActors;
|
||||
const newCscaRoot = generateRandomFieldElement();
|
||||
|
||||
await expect(registry.connect(user1).updateCscaRoot(newCscaRoot)).to.be.revertedWithCustomError(
|
||||
registry,
|
||||
"OwnableUnauthorizedAccount",
|
||||
"AccessControlUnauthorizedAccount",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -498,7 +498,7 @@ describe("Unit Tests for IdentityRegistry", () => {
|
||||
|
||||
await expect(
|
||||
registry.connect(user1).devAddIdentityCommitment(attestationId, nullifier, commitment),
|
||||
).to.be.revertedWithCustomError(registry, "OwnableUnauthorizedAccount");
|
||||
).to.be.revertedWithCustomError(registry, "AccessControlUnauthorizedAccount");
|
||||
});
|
||||
|
||||
it("should not add commitment if caller is not proxy", async () => {
|
||||
@@ -546,7 +546,7 @@ describe("Unit Tests for IdentityRegistry", () => {
|
||||
const newCommitment = generateRandomFieldElement();
|
||||
await expect(
|
||||
registry.connect(user1).devUpdateCommitment(commitment, newCommitment, []),
|
||||
).to.be.revertedWithCustomError(registry, "OwnableUnauthorizedAccount");
|
||||
).to.be.revertedWithCustomError(registry, "AccessControlUnauthorizedAccount");
|
||||
});
|
||||
|
||||
it("should not update commitment if caller is not proxy", async () => {
|
||||
@@ -592,7 +592,7 @@ describe("Unit Tests for IdentityRegistry", () => {
|
||||
await registry.devAddIdentityCommitment(attestationId, nullifier, commitment);
|
||||
await expect(registry.connect(user1).devRemoveCommitment(commitment, [])).to.be.revertedWithCustomError(
|
||||
registry,
|
||||
"OwnableUnauthorizedAccount",
|
||||
"AccessControlUnauthorizedAccount",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -632,7 +632,7 @@ describe("Unit Tests for IdentityRegistry", () => {
|
||||
const dscCommitment = generateRandomFieldElement();
|
||||
await expect(registry.connect(user1).devAddDscKeyCommitment(dscCommitment)).to.be.revertedWithCustomError(
|
||||
registry,
|
||||
"OwnableUnauthorizedAccount",
|
||||
"AccessControlUnauthorizedAccount",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -673,7 +673,7 @@ describe("Unit Tests for IdentityRegistry", () => {
|
||||
await registry.devAddDscKeyCommitment(dscCommitment);
|
||||
await expect(
|
||||
registry.connect(user1).devUpdateDscKeyCommitment(dscCommitment, newDscCommitment, []),
|
||||
).to.be.revertedWithCustomError(registry, "OwnableUnauthorizedAccount");
|
||||
).to.be.revertedWithCustomError(registry, "AccessControlUnauthorizedAccount");
|
||||
});
|
||||
|
||||
it("should not update dsc key commitment if caller is not proxy", async () => {
|
||||
@@ -711,7 +711,7 @@ describe("Unit Tests for IdentityRegistry", () => {
|
||||
await registry.devAddDscKeyCommitment(dscCommitment);
|
||||
await expect(registry.connect(user1).devRemoveDscKeyCommitment(dscCommitment, [])).to.be.revertedWithCustomError(
|
||||
registry,
|
||||
"OwnableUnauthorizedAccount",
|
||||
"AccessControlUnauthorizedAccount",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -751,7 +751,7 @@ describe("Unit Tests for IdentityRegistry", () => {
|
||||
const nullifier = generateRandomFieldElement();
|
||||
await expect(
|
||||
registry.connect(user1).devChangeNullifierState(attestationId, nullifier, false),
|
||||
).to.be.revertedWithCustomError(registry, "OwnableUnauthorizedAccount");
|
||||
).to.be.revertedWithCustomError(registry, "AccessControlUnauthorizedAccount");
|
||||
});
|
||||
|
||||
it("should not change nullifier state if caller is not proxy", async () => {
|
||||
@@ -789,7 +789,7 @@ describe("Unit Tests for IdentityRegistry", () => {
|
||||
const state = true;
|
||||
await expect(
|
||||
registry.connect(user1).devChangeDscKeyCommitmentState(dscCommitment, state),
|
||||
).to.be.revertedWithCustomError(registry, "OwnableUnauthorizedAccount");
|
||||
).to.be.revertedWithCustomError(registry, "AccessControlUnauthorizedAccount");
|
||||
});
|
||||
|
||||
it("should not change dsc key commitment state if caller is not proxy", async () => {
|
||||
@@ -915,7 +915,7 @@ describe("Unit Tests for IdentityRegistry", () => {
|
||||
|
||||
await expect(
|
||||
registry.connect(user1).upgradeToAndCall(registryV2Implementation.target, "0x"),
|
||||
).to.be.revertedWithCustomError(registry, "OwnableUnauthorizedAccount");
|
||||
).to.be.revertedWithCustomError(registry, "AccessControlUnauthorizedAccount");
|
||||
});
|
||||
|
||||
it("should not allow implementation contract to be initialized directly", async () => {
|
||||
|
||||
@@ -1,95 +1,272 @@
|
||||
import { expect } from "chai";
|
||||
import { ethers } from "hardhat";
|
||||
import { ZeroAddress } from "ethers";
|
||||
import { MockImplRoot } from "../../typechain-types";
|
||||
import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers";
|
||||
|
||||
describe("ImplRoot", () => {
|
||||
let mockImplRoot: MockImplRoot;
|
||||
let owner: any;
|
||||
let user1: any;
|
||||
let deployer: SignerWithAddress;
|
||||
let securityMultisig: SignerWithAddress;
|
||||
let operationsMultisig: SignerWithAddress;
|
||||
let user1: SignerWithAddress;
|
||||
|
||||
const SECURITY_ROLE = ethers.keccak256(ethers.toUtf8Bytes("SECURITY_ROLE"));
|
||||
const OPERATIONS_ROLE = ethers.keccak256(ethers.toUtf8Bytes("OPERATIONS_ROLE"));
|
||||
|
||||
beforeEach(async () => {
|
||||
[owner, user1] = await ethers.getSigners();
|
||||
[deployer, securityMultisig, operationsMultisig, user1] = await ethers.getSigners();
|
||||
|
||||
const MockImplRootFactory = await ethers.getContractFactory("MockImplRoot", owner);
|
||||
const MockImplRootFactory = await ethers.getContractFactory("MockImplRoot", deployer);
|
||||
mockImplRoot = await MockImplRootFactory.deploy();
|
||||
await mockImplRoot.waitForDeployment();
|
||||
});
|
||||
|
||||
describe("Role Constants", () => {
|
||||
it("should have correct role constants", async () => {
|
||||
expect(await mockImplRoot.SECURITY_ROLE()).to.equal(SECURITY_ROLE);
|
||||
expect(await mockImplRoot.OPERATIONS_ROLE()).to.equal(OPERATIONS_ROLE);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Initialization", () => {
|
||||
it("should revert when calling __ImplRoot_init outside initialization phase", async () => {
|
||||
// First initialize the contract properly
|
||||
await mockImplRoot.exposed__ImplRoot_init();
|
||||
|
||||
// Then try to initialize again - this should fail
|
||||
await expect(mockImplRoot.exposed__ImplRoot_init()).to.be.revertedWithCustomError(
|
||||
mockImplRoot,
|
||||
"NotInitializing",
|
||||
);
|
||||
});
|
||||
|
||||
it("should revert when initializing with zero address owner", async () => {
|
||||
await expect(mockImplRoot.exposed__Ownable_init(ZeroAddress))
|
||||
.to.be.revertedWithCustomError(mockImplRoot, "OwnableInvalidOwner")
|
||||
.withArgs(ZeroAddress);
|
||||
});
|
||||
|
||||
it("should set correct owner when initializing with valid address", async () => {
|
||||
await mockImplRoot.exposed__Ownable_init(owner.address);
|
||||
expect(await mockImplRoot.owner()).to.equal(owner.address);
|
||||
});
|
||||
|
||||
it("should revert when initializing twice", async () => {
|
||||
await mockImplRoot.exposed__Ownable_init(owner.address);
|
||||
|
||||
await expect(mockImplRoot.exposed__Ownable_init(owner.address)).to.be.revertedWithCustomError(
|
||||
mockImplRoot,
|
||||
"InvalidInitialization",
|
||||
);
|
||||
});
|
||||
|
||||
it("should initialize with deployer having both roles", async () => {
|
||||
// Deploy a fresh contract for initialization testing
|
||||
const MockImplRootFactory = await ethers.getContractFactory("MockImplRoot");
|
||||
const freshContract = await MockImplRootFactory.deploy();
|
||||
await freshContract.waitForDeployment();
|
||||
|
||||
await freshContract.exposed__ImplRoot_init();
|
||||
|
||||
// Check role assignments - deployer should have both roles
|
||||
expect(await freshContract.hasRole(SECURITY_ROLE, deployer.address)).to.be.true;
|
||||
expect(await freshContract.hasRole(OPERATIONS_ROLE, deployer.address)).to.be.true;
|
||||
|
||||
// Check role admins - SECURITY_ROLE manages all roles
|
||||
expect(await freshContract.getRoleAdmin(SECURITY_ROLE)).to.equal(SECURITY_ROLE);
|
||||
expect(await freshContract.getRoleAdmin(OPERATIONS_ROLE)).to.equal(SECURITY_ROLE);
|
||||
});
|
||||
|
||||
it("should allow role transfer after initialization", async () => {
|
||||
// Deploy a fresh contract for initialization testing
|
||||
const MockImplRootFactory = await ethers.getContractFactory("MockImplRoot");
|
||||
const freshContract = await MockImplRootFactory.deploy();
|
||||
await freshContract.waitForDeployment();
|
||||
|
||||
await freshContract.exposed__ImplRoot_init();
|
||||
|
||||
// Transfer roles to multisigs
|
||||
await freshContract.connect(deployer).grantRole(SECURITY_ROLE, securityMultisig.address);
|
||||
await freshContract.connect(deployer).grantRole(OPERATIONS_ROLE, operationsMultisig.address);
|
||||
|
||||
// Verify multisigs have roles
|
||||
expect(await freshContract.hasRole(SECURITY_ROLE, securityMultisig.address)).to.be.true;
|
||||
expect(await freshContract.hasRole(OPERATIONS_ROLE, operationsMultisig.address)).to.be.true;
|
||||
|
||||
// Deployer can renounce roles
|
||||
await freshContract.connect(deployer).renounceRole(SECURITY_ROLE, deployer.address);
|
||||
await freshContract.connect(deployer).renounceRole(OPERATIONS_ROLE, deployer.address);
|
||||
|
||||
// Verify deployer no longer has roles
|
||||
expect(await freshContract.hasRole(SECURITY_ROLE, deployer.address)).to.be.false;
|
||||
expect(await freshContract.hasRole(OPERATIONS_ROLE, deployer.address)).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe("Role Management", () => {
|
||||
let initializedContract: MockImplRoot;
|
||||
|
||||
beforeEach(async () => {
|
||||
const MockImplRootFactory = await ethers.getContractFactory("MockImplRoot");
|
||||
initializedContract = await MockImplRootFactory.deploy();
|
||||
await initializedContract.waitForDeployment();
|
||||
|
||||
// Initialize with deployer having roles, then transfer to multisigs
|
||||
await initializedContract.exposed__ImplRoot_init();
|
||||
await initializedContract.connect(deployer).grantRole(SECURITY_ROLE, securityMultisig.address);
|
||||
await initializedContract.connect(deployer).grantRole(OPERATIONS_ROLE, operationsMultisig.address);
|
||||
});
|
||||
|
||||
it("should allow critical multisig to grant roles", async () => {
|
||||
await expect(initializedContract.connect(securityMultisig).grantRole(OPERATIONS_ROLE, user1.address)).to.not.be
|
||||
.reverted;
|
||||
|
||||
expect(await initializedContract.hasRole(OPERATIONS_ROLE, user1.address)).to.be.true;
|
||||
});
|
||||
|
||||
it("should allow critical multisig to revoke roles", async () => {
|
||||
// First grant a role to user1
|
||||
await initializedContract.connect(securityMultisig).grantRole(OPERATIONS_ROLE, user1.address);
|
||||
expect(await initializedContract.hasRole(OPERATIONS_ROLE, user1.address)).to.be.true;
|
||||
|
||||
// Then revoke it
|
||||
await expect(initializedContract.connect(securityMultisig).revokeRole(OPERATIONS_ROLE, user1.address)).to.not.be
|
||||
.reverted;
|
||||
|
||||
expect(await initializedContract.hasRole(OPERATIONS_ROLE, user1.address)).to.be.false;
|
||||
});
|
||||
|
||||
it("should prevent standard multisig from granting critical role", async () => {
|
||||
await expect(
|
||||
initializedContract.connect(operationsMultisig).grantRole(SECURITY_ROLE, user1.address),
|
||||
).to.be.revertedWithCustomError(initializedContract, "AccessControlUnauthorizedAccount");
|
||||
});
|
||||
|
||||
it("should prevent unauthorized users from granting roles", async () => {
|
||||
await expect(
|
||||
initializedContract.connect(user1).grantRole(OPERATIONS_ROLE, user1.address),
|
||||
).to.be.revertedWithCustomError(initializedContract, "AccessControlUnauthorizedAccount");
|
||||
});
|
||||
|
||||
it("should allow role holders to renounce their own roles", async () => {
|
||||
// Grant role to user1
|
||||
await initializedContract.connect(securityMultisig).grantRole(OPERATIONS_ROLE, user1.address);
|
||||
expect(await initializedContract.hasRole(OPERATIONS_ROLE, user1.address)).to.be.true;
|
||||
|
||||
// User1 can renounce their own role
|
||||
await expect(initializedContract.connect(user1).renounceRole(OPERATIONS_ROLE, user1.address)).to.not.be.reverted;
|
||||
|
||||
expect(await initializedContract.hasRole(OPERATIONS_ROLE, user1.address)).to.be.false;
|
||||
});
|
||||
|
||||
it("should prevent users from renouncing others' roles", async () => {
|
||||
await expect(
|
||||
initializedContract.connect(user1).renounceRole(SECURITY_ROLE, securityMultisig.address),
|
||||
).to.be.revertedWithCustomError(initializedContract, "AccessControlBadConfirmation");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Upgrade Authorization", () => {
|
||||
let proxy: any;
|
||||
let implContract: any;
|
||||
let initializedContract: MockImplRoot;
|
||||
|
||||
beforeEach(async () => {
|
||||
const MockImplRootFactory = await ethers.getContractFactory("MockImplRoot", owner);
|
||||
implContract = await MockImplRootFactory.deploy();
|
||||
await implContract.waitForDeployment();
|
||||
const MockImplRootFactory = await ethers.getContractFactory("MockImplRoot");
|
||||
initializedContract = await MockImplRootFactory.deploy();
|
||||
await initializedContract.waitForDeployment();
|
||||
|
||||
const initData = implContract.interface.encodeFunctionData("exposed__Ownable_init", [owner.address]);
|
||||
|
||||
const ProxyFactory = await ethers.getContractFactory("ERC1967Proxy");
|
||||
proxy = await ProxyFactory.deploy(implContract.target, initData);
|
||||
await proxy.waitForDeployment();
|
||||
|
||||
mockImplRoot = await ethers.getContractAt("MockImplRoot", proxy.target);
|
||||
// Initialize and transfer roles
|
||||
await initializedContract.exposed__ImplRoot_init();
|
||||
await initializedContract.connect(deployer).grantRole(SECURITY_ROLE, securityMultisig.address);
|
||||
await initializedContract.connect(deployer).grantRole(OPERATIONS_ROLE, operationsMultisig.address);
|
||||
});
|
||||
|
||||
it("should revert when calling _authorizeUpgrade from non-proxy", async () => {
|
||||
const MockImplRootFactory = await ethers.getContractFactory("MockImplRoot", owner);
|
||||
const newImpl = await MockImplRootFactory.deploy();
|
||||
await newImpl.waitForDeployment();
|
||||
it("should allow critical multisig to authorize upgrades", async () => {
|
||||
const newImplementation = ethers.Wallet.createRandom().address;
|
||||
|
||||
await expect(implContract.exposed_authorizeUpgrade(newImpl.target)).to.be.revertedWithCustomError(
|
||||
implContract,
|
||||
"UUPSUnauthorizedCallContext",
|
||||
// Note: _authorizeUpgrade is internal and can only be called through proxy upgrade mechanism
|
||||
// We test this by verifying the critical multisig has the required role
|
||||
expect(await initializedContract.hasRole(SECURITY_ROLE, securityMultisig.address)).to.be.true;
|
||||
});
|
||||
|
||||
it("should prevent standard multisig from authorizing upgrades", async () => {
|
||||
const newImplementation = ethers.Wallet.createRandom().address;
|
||||
|
||||
// Standard multisig should not have SECURITY_ROLE
|
||||
expect(await initializedContract.hasRole(SECURITY_ROLE, operationsMultisig.address)).to.be.false;
|
||||
});
|
||||
|
||||
it("should prevent unauthorized users from authorizing upgrades", async () => {
|
||||
const newImplementation = ethers.Wallet.createRandom().address;
|
||||
|
||||
// Unauthorized users should not have SECURITY_ROLE
|
||||
expect(await initializedContract.hasRole(SECURITY_ROLE, user1.address)).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe("Role Hierarchy", () => {
|
||||
let initializedContract: MockImplRoot;
|
||||
|
||||
beforeEach(async () => {
|
||||
const MockImplRootFactory = await ethers.getContractFactory("MockImplRoot");
|
||||
initializedContract = await MockImplRootFactory.deploy();
|
||||
await initializedContract.waitForDeployment();
|
||||
|
||||
await initializedContract.exposed__ImplRoot_init();
|
||||
});
|
||||
|
||||
it("should have SECURITY_ROLE as admin of both roles", async () => {
|
||||
expect(await initializedContract.getRoleAdmin(SECURITY_ROLE)).to.equal(SECURITY_ROLE);
|
||||
expect(await initializedContract.getRoleAdmin(OPERATIONS_ROLE)).to.equal(SECURITY_ROLE);
|
||||
});
|
||||
|
||||
it("should allow SECURITY_ROLE holders to manage OPERATIONS_ROLE", async () => {
|
||||
// Grant SECURITY_ROLE to securityMultisig
|
||||
await initializedContract.connect(deployer).grantRole(SECURITY_ROLE, securityMultisig.address);
|
||||
|
||||
// Critical multisig should be able to grant OPERATIONS_ROLE
|
||||
await expect(initializedContract.connect(securityMultisig).grantRole(OPERATIONS_ROLE, user1.address)).to.not.be
|
||||
.reverted;
|
||||
|
||||
expect(await initializedContract.hasRole(OPERATIONS_ROLE, user1.address)).to.be.true;
|
||||
|
||||
// Critical multisig should be able to revoke OPERATIONS_ROLE
|
||||
await expect(initializedContract.connect(securityMultisig).revokeRole(OPERATIONS_ROLE, user1.address)).to.not.be
|
||||
.reverted;
|
||||
|
||||
expect(await initializedContract.hasRole(OPERATIONS_ROLE, user1.address)).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe("Complete Workflow", () => {
|
||||
it("should demonstrate complete deployment and role transfer workflow", async () => {
|
||||
console.log("\n🔄 Starting Complete ImplRoot Workflow");
|
||||
|
||||
// 1. Deploy contract
|
||||
const MockImplRootFactory = await ethers.getContractFactory("MockImplRoot");
|
||||
const contract = await MockImplRootFactory.deploy();
|
||||
await contract.waitForDeployment();
|
||||
|
||||
console.log("✅ Step 1: Contract deployed");
|
||||
|
||||
// 2. Initialize with deployer having roles
|
||||
await contract.exposed__ImplRoot_init();
|
||||
|
||||
console.log("✅ Step 2: Contract initialized");
|
||||
console.log(` - Deployer has SECURITY_ROLE: ${await contract.hasRole(SECURITY_ROLE, deployer.address)}`);
|
||||
console.log(` - Deployer has OPERATIONS_ROLE: ${await contract.hasRole(OPERATIONS_ROLE, deployer.address)}`);
|
||||
|
||||
// 3. Grant roles to multisigs
|
||||
await contract.connect(deployer).grantRole(SECURITY_ROLE, securityMultisig.address);
|
||||
await contract.connect(deployer).grantRole(OPERATIONS_ROLE, operationsMultisig.address);
|
||||
|
||||
console.log("✅ Step 3: Roles granted to multisigs");
|
||||
console.log(
|
||||
` - Critical multisig has SECURITY_ROLE: ${await contract.hasRole(SECURITY_ROLE, securityMultisig.address)}`,
|
||||
);
|
||||
console.log(
|
||||
` - Standard multisig has OPERATIONS_ROLE: ${await contract.hasRole(OPERATIONS_ROLE, operationsMultisig.address)}`,
|
||||
);
|
||||
});
|
||||
|
||||
it("should revert when non-owner calls _authorizeUpgrade", async () => {
|
||||
const MockImplRootFactory = await ethers.getContractFactory("MockImplRoot", owner);
|
||||
const newImpl = await MockImplRootFactory.deploy();
|
||||
await newImpl.waitForDeployment();
|
||||
// 4. Verify multisigs can operate (check role permissions)
|
||||
expect(await contract.hasRole(SECURITY_ROLE, securityMultisig.address)).to.be.true;
|
||||
|
||||
await expect(mockImplRoot.connect(user1).exposed_authorizeUpgrade(newImpl.target))
|
||||
.to.be.revertedWithCustomError(mockImplRoot, "OwnableUnauthorizedAccount")
|
||||
.withArgs(user1.address);
|
||||
});
|
||||
console.log("✅ Step 4: Multisigs verified functional");
|
||||
|
||||
it("should allow owner to call _authorizeUpgrade through proxy", async () => {
|
||||
const MockImplRootFactory = await ethers.getContractFactory("MockImplRoot", owner);
|
||||
const newImpl = await MockImplRootFactory.deploy();
|
||||
await newImpl.waitForDeployment();
|
||||
// 5. Renounce deployer roles
|
||||
await contract.connect(deployer).renounceRole(SECURITY_ROLE, deployer.address);
|
||||
await contract.connect(deployer).renounceRole(OPERATIONS_ROLE, deployer.address);
|
||||
|
||||
await expect(mockImplRoot.connect(owner).exposed_authorizeUpgrade(newImpl.target)).to.not.be.reverted;
|
||||
console.log("✅ Step 5: Deployer roles renounced");
|
||||
console.log(` - Deployer has SECURITY_ROLE: ${await contract.hasRole(SECURITY_ROLE, deployer.address)}`);
|
||||
console.log(` - Deployer has OPERATIONS_ROLE: ${await contract.hasRole(OPERATIONS_ROLE, deployer.address)}`);
|
||||
|
||||
// 6. Final verification
|
||||
expect(await contract.hasRole(SECURITY_ROLE, securityMultisig.address)).to.be.true;
|
||||
expect(await contract.hasRole(OPERATIONS_ROLE, operationsMultisig.address)).to.be.true;
|
||||
expect(await contract.hasRole(SECURITY_ROLE, deployer.address)).to.be.false;
|
||||
expect(await contract.hasRole(OPERATIONS_ROLE, deployer.address)).to.be.false;
|
||||
|
||||
console.log("🎉 Complete ImplRoot workflow successful!");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,9 +8,14 @@ describe("PCR0Manager", function () {
|
||||
let owner: SignerWithAddress;
|
||||
let other: SignerWithAddress;
|
||||
|
||||
// Sample PCR0 value for testing (48 bytes)
|
||||
const samplePCR0 = "0x" + "00".repeat(48);
|
||||
const invalidPCR0 = "0x" + "00".repeat(32); // 32 bytes (invalid size)
|
||||
// Sample PCR0 value for testing
|
||||
// addPCR0/removePCR0 expect 32 bytes (GCP image hash)
|
||||
const samplePCR0_32bytes = "0x" + "ab".repeat(32);
|
||||
// isPCR0Set expects 48 bytes (16 zero bytes prefix + 32 byte hash, for mobile compatibility)
|
||||
const samplePCR0_48bytes = "0x" + "00".repeat(16) + "ab".repeat(32);
|
||||
// Invalid sizes for testing error cases
|
||||
const invalidPCR0_for_add = "0x" + "00".repeat(48); // 48 bytes - invalid for add/remove
|
||||
const invalidPCR0_for_check = "0x" + "00".repeat(32); // 32 bytes - invalid for isPCR0Set
|
||||
|
||||
beforeEach(async function () {
|
||||
[owner, other] = await ethers.getSigners();
|
||||
@@ -21,74 +26,68 @@ describe("PCR0Manager", function () {
|
||||
|
||||
describe("addPCR0", function () {
|
||||
it("should allow owner to add PCR0 value", async function () {
|
||||
await expect(pcr0Manager.addPCR0(samplePCR0)).to.emit(pcr0Manager, "PCR0Added");
|
||||
await expect(pcr0Manager.addPCR0(samplePCR0_32bytes)).to.emit(pcr0Manager, "PCR0Added");
|
||||
|
||||
expect(await pcr0Manager.isPCR0Set(samplePCR0)).to.be.true;
|
||||
});
|
||||
|
||||
it("should allow owner to add PCR0 value", async function () {
|
||||
await expect(pcr0Manager.addPCR0(samplePCR0)).to.emit(pcr0Manager, "PCR0Added");
|
||||
|
||||
expect(await pcr0Manager.isPCR0Set(samplePCR0)).to.be.true;
|
||||
expect(await pcr0Manager.isPCR0Set(samplePCR0_48bytes)).to.be.true;
|
||||
});
|
||||
|
||||
it("should not allow non-owner to add PCR0 value", async function () {
|
||||
await expect(pcr0Manager.connect(other).addPCR0(samplePCR0))
|
||||
.to.be.revertedWithCustomError(pcr0Manager, "OwnableUnauthorizedAccount")
|
||||
.withArgs(other.address);
|
||||
await expect(pcr0Manager.connect(other).addPCR0(samplePCR0_32bytes))
|
||||
.to.be.revertedWithCustomError(pcr0Manager, "AccessControlUnauthorizedAccount")
|
||||
.withArgs(other.address, await pcr0Manager.SECURITY_ROLE());
|
||||
});
|
||||
|
||||
it("should not allow adding PCR0 with invalid size", async function () {
|
||||
await expect(pcr0Manager.addPCR0(invalidPCR0)).to.be.revertedWith("PCR0 must be 48 bytes");
|
||||
await expect(pcr0Manager.addPCR0(invalidPCR0_for_add)).to.be.revertedWith("PCR0 must be 32 bytes");
|
||||
});
|
||||
|
||||
it("should not allow adding duplicate PCR0", async function () {
|
||||
await pcr0Manager.addPCR0(samplePCR0);
|
||||
await expect(pcr0Manager.addPCR0(samplePCR0)).to.be.revertedWith("PCR0 already set");
|
||||
await pcr0Manager.addPCR0(samplePCR0_32bytes);
|
||||
await expect(pcr0Manager.addPCR0(samplePCR0_32bytes)).to.be.revertedWith("PCR0 already set");
|
||||
});
|
||||
});
|
||||
|
||||
describe("removePCR0", function () {
|
||||
beforeEach(async function () {
|
||||
await pcr0Manager.addPCR0(samplePCR0);
|
||||
await pcr0Manager.addPCR0(samplePCR0_32bytes);
|
||||
});
|
||||
|
||||
it("should allow owner to remove PCR0 value", async function () {
|
||||
await expect(pcr0Manager.removePCR0(samplePCR0)).to.emit(pcr0Manager, "PCR0Removed");
|
||||
await expect(pcr0Manager.removePCR0(samplePCR0_32bytes)).to.emit(pcr0Manager, "PCR0Removed");
|
||||
|
||||
expect(await pcr0Manager.isPCR0Set(samplePCR0)).to.be.false;
|
||||
expect(await pcr0Manager.isPCR0Set(samplePCR0_48bytes)).to.be.false;
|
||||
});
|
||||
|
||||
// This is not actually needed, just for increase the coverage of the test code
|
||||
it("should not allow remove PCR0 with invalid size", async function () {
|
||||
await expect(pcr0Manager.removePCR0(invalidPCR0)).to.be.revertedWith("PCR0 must be 48 bytes");
|
||||
await expect(pcr0Manager.removePCR0(invalidPCR0_for_add)).to.be.revertedWith("PCR0 must be 32 bytes");
|
||||
});
|
||||
|
||||
it("should not allow non-owner to remove PCR0 value", async function () {
|
||||
await expect(pcr0Manager.connect(other).removePCR0(samplePCR0))
|
||||
.to.be.revertedWithCustomError(pcr0Manager, "OwnableUnauthorizedAccount")
|
||||
.withArgs(other.address);
|
||||
await expect(pcr0Manager.connect(other).removePCR0(samplePCR0_32bytes))
|
||||
.to.be.revertedWithCustomError(pcr0Manager, "AccessControlUnauthorizedAccount")
|
||||
.withArgs(other.address, await pcr0Manager.SECURITY_ROLE());
|
||||
});
|
||||
|
||||
it("should not allow removing non-existent PCR0", async function () {
|
||||
const otherPCR0 = "0x" + "11".repeat(48);
|
||||
const otherPCR0 = "0x" + "11".repeat(32);
|
||||
await expect(pcr0Manager.removePCR0(otherPCR0)).to.be.revertedWith("PCR0 not set");
|
||||
});
|
||||
});
|
||||
|
||||
describe("isPCR0Set", function () {
|
||||
it("should correctly return PCR0 status", async function () {
|
||||
expect(await pcr0Manager.isPCR0Set(samplePCR0)).to.be.false;
|
||||
expect(await pcr0Manager.isPCR0Set(samplePCR0_48bytes)).to.be.false;
|
||||
|
||||
await pcr0Manager.addPCR0(samplePCR0);
|
||||
expect(await pcr0Manager.isPCR0Set(samplePCR0)).to.be.true;
|
||||
await pcr0Manager.addPCR0(samplePCR0_32bytes);
|
||||
expect(await pcr0Manager.isPCR0Set(samplePCR0_48bytes)).to.be.true;
|
||||
|
||||
await pcr0Manager.removePCR0(samplePCR0);
|
||||
expect(await pcr0Manager.isPCR0Set(samplePCR0)).to.be.false;
|
||||
await pcr0Manager.removePCR0(samplePCR0_32bytes);
|
||||
expect(await pcr0Manager.isPCR0Set(samplePCR0_48bytes)).to.be.false;
|
||||
});
|
||||
|
||||
it("should not allow checking PCR0 with invalid size", async function () {
|
||||
await expect(pcr0Manager.isPCR0Set(invalidPCR0)).to.be.revertedWith("PCR0 must be 48 bytes");
|
||||
await expect(pcr0Manager.isPCR0Set(invalidPCR0_for_check)).to.be.revertedWith("PCR0 must be 48 bytes");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -149,9 +149,11 @@ describe("Aadhaar Registration test", function () {
|
||||
});
|
||||
|
||||
it("should not fail if timestamp is within 20 minutes", async () => {
|
||||
// Fix the AADHAAR_REGISTRATION_WINDOW that was incorrectly set to 0
|
||||
await deployedActors.hub.setAadhaarRegistrationWindow(20);
|
||||
|
||||
const latestBlock = await ethers.provider.getBlock("latest");
|
||||
const blockTimestamp = latestBlock!.timestamp;
|
||||
|
||||
const newAadhaarData = prepareAadhaarRegisterTestData(
|
||||
privateKeyPem,
|
||||
pubkeyPem,
|
||||
@@ -161,8 +163,7 @@ describe("Aadhaar Registration test", function () {
|
||||
"M",
|
||||
"110051",
|
||||
"WB",
|
||||
//timestamp 10 minutes ago and converted to timestamp string
|
||||
new Date(Date.now() - 10 * 60 * 1000).getTime().toString(),
|
||||
(blockTimestamp - 10 * 60).toString(),
|
||||
);
|
||||
const newRegisterProof = await generateRegisterAadhaarProof(registerSecret, newAadhaarData.inputs);
|
||||
|
||||
@@ -171,9 +172,11 @@ describe("Aadhaar Registration test", function () {
|
||||
});
|
||||
|
||||
it("should fail with InvalidUidaiTimestamp when UIDAI timestamp is not within 20 minutes of current time", async () => {
|
||||
// Fix the AADHAAR_REGISTRATION_WINDOW that was incorrectly set to 0
|
||||
await deployedActors.hub.setAadhaarRegistrationWindow(20);
|
||||
|
||||
const latestBlock = await ethers.provider.getBlock("latest");
|
||||
const blockTimestamp = latestBlock!.timestamp;
|
||||
|
||||
const newAadhaarData = prepareAadhaarRegisterTestData(
|
||||
privateKeyPem,
|
||||
pubkeyPem,
|
||||
@@ -183,8 +186,7 @@ describe("Aadhaar Registration test", function () {
|
||||
"M",
|
||||
"110051",
|
||||
"WB",
|
||||
//timestamp 30 minutes ago and converted to timestamp string
|
||||
new Date(Date.now() - 30 * 60 * 1000).getTime().toString(),
|
||||
(blockTimestamp - 30 * 60).toString(),
|
||||
);
|
||||
const newRegisterProof = await generateRegisterAadhaarProof(registerSecret, newAadhaarData.inputs);
|
||||
|
||||
|
||||
@@ -37,7 +37,6 @@
|
||||
"@babel/core": "^7.28.4",
|
||||
"@babel/runtime": "^7.28.4",
|
||||
"@noble/curves": "1.9.7",
|
||||
"@noble/hashes": "1.8.0",
|
||||
"@swc/core": "1.7.36",
|
||||
"@tamagui/animations-react-native": "1.126.14",
|
||||
"@tamagui/toast": "1.126.14",
|
||||
|
||||
@@ -151,7 +151,7 @@
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.28.3",
|
||||
"@selfxyz/common": "workspace:^",
|
||||
"@selfxyz/euclid": "^0.4.1",
|
||||
"@selfxyz/euclid": "^0.6.0",
|
||||
"@xstate/react": "^5.0.5",
|
||||
"node-forge": "^1.3.1",
|
||||
"react-native-nfc-manager": "^3.17.1",
|
||||
|
||||
@@ -18,6 +18,7 @@ const BRANCH = 'main';
|
||||
// Environment detection
|
||||
const isCI = process.env.CI === 'true';
|
||||
const repoToken = process.env.SELFXYZ_INTERNAL_REPO_PAT;
|
||||
const appToken = process.env.SELFXYZ_APP_TOKEN; // GitHub App installation token
|
||||
const isDryRun = process.env.DRY_RUN === 'true';
|
||||
|
||||
function log(message, type = 'info') {
|
||||
@@ -89,19 +90,24 @@ function setupSubmodule() {
|
||||
|
||||
let submoduleUrl;
|
||||
|
||||
if (isCI && repoToken) {
|
||||
if (isCI && appToken) {
|
||||
// CI environment with GitHub App installation token
|
||||
// Security: NEVER embed credentials in git URLs. Rely on CI-provided auth via:
|
||||
// - ~/.netrc, a Git credential helper, or SSH agent configuration.
|
||||
submoduleUrl = `https://github.com/${GITHUB_ORG}/${REPO_NAME}.git`;
|
||||
} else if (isCI && repoToken) {
|
||||
// CI environment with Personal Access Token
|
||||
log('CI detected: Using SELFXYZ_INTERNAL_REPO_PAT for submodule', 'info');
|
||||
submoduleUrl = `https://${repoToken}@github.com/${GITHUB_ORG}/${REPO_NAME}.git`;
|
||||
// Security: NEVER embed credentials in git URLs. Rely on CI-provided auth via:
|
||||
// - ~/.netrc, a Git credential helper, or SSH agent configuration.
|
||||
submoduleUrl = `https://github.com/${GITHUB_ORG}/${REPO_NAME}.git`;
|
||||
} else if (isCI) {
|
||||
log('CI environment detected but SELFXYZ_INTERNAL_REPO_PAT not available - skipping private module setup', 'info');
|
||||
log('CI environment detected but no token available - skipping private module setup', 'info');
|
||||
log('This is expected for forked PRs or environments without access to private modules', 'info');
|
||||
return false; // Return false to indicate setup was skipped
|
||||
} else if (usingHTTPSGitAuth()) {
|
||||
submoduleUrl = `https://github.com/${GITHUB_ORG}/${REPO_NAME}.git`;
|
||||
} else {
|
||||
// Local development with SSH
|
||||
log('Local development: Using SSH for submodule', 'info');
|
||||
submoduleUrl = `git@github.com:${GITHUB_ORG}/${REPO_NAME}.git`;
|
||||
}
|
||||
|
||||
@@ -113,7 +119,7 @@ function setupSubmodule() {
|
||||
} else {
|
||||
// Add submodule
|
||||
const addCommand = `git submodule add -b ${BRANCH} "${submoduleUrl}" mobile-sdk-native`;
|
||||
if (isCI && repoToken) {
|
||||
if (isCI && (appToken || repoToken)) {
|
||||
// Security: Run command silently to avoid token exposure in logs
|
||||
runCommand(addCommand, { stdio: 'pipe' });
|
||||
} else {
|
||||
@@ -125,7 +131,7 @@ function setupSubmodule() {
|
||||
return true; // Return true to indicate successful setup
|
||||
} catch (error) {
|
||||
if (isCI) {
|
||||
log('Submodule setup failed in CI environment. Check SELFXYZ_INTERNAL_REPO_PAT permissions.', 'error');
|
||||
log('Submodule setup failed in CI environment. Check repository access/credentials configuration.', 'error');
|
||||
} else {
|
||||
log('Submodule setup failed. Ensure you have SSH access to the repository.', 'error');
|
||||
}
|
||||
@@ -169,7 +175,7 @@ function setupMobileSDKNative() {
|
||||
}
|
||||
|
||||
// Security: Remove credential-embedded remote URL after setup
|
||||
if (isCI && repoToken && !isDryRun) {
|
||||
if (isCI && (appToken || repoToken) && !isDryRun) {
|
||||
scrubGitRemoteUrl();
|
||||
}
|
||||
|
||||
|
||||
@@ -79,6 +79,7 @@ export const BackupEvents = {
|
||||
};
|
||||
|
||||
export const DocumentEvents = {
|
||||
COUNTRY_HELP_TAPPED: 'Document: Country Help Tapped',
|
||||
ADD_NEW_AADHAAR_SELECTED: 'Document: Add Aadhaar',
|
||||
ADD_NEW_MOCK_SELECTED: 'Document: Add New Document via Mock',
|
||||
ADD_NEW_SCAN_SELECTED: 'Document: Add New Document via Scan',
|
||||
|
||||
@@ -5,15 +5,18 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { commonNames } from '@selfxyz/common/constants/countries';
|
||||
import { CountryPickerScreen as CountryPickerUI } from '@selfxyz/euclid';
|
||||
import { CountryPickerScreen as CountryPickerUI, type SafeArea } from '@selfxyz/euclid';
|
||||
|
||||
import { RoundFlag } from '../../components';
|
||||
import { DocumentEvents } from '../../constants/analytics';
|
||||
import { useSelfClient } from '../../context';
|
||||
import { useCountries } from '../../documents/useCountries';
|
||||
import { buttonTap } from '../../haptic';
|
||||
import { SdkEvents } from '../../types/events';
|
||||
|
||||
const CountryPickerScreen: React.FC = () => {
|
||||
const CountryPickerScreen: React.FC<SafeArea> & { statusBar: typeof CountryPickerUI.statusBar } = ({
|
||||
insets,
|
||||
}: SafeArea) => {
|
||||
const selfClient = useSelfClient();
|
||||
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
@@ -57,9 +60,9 @@ const CountryPickerScreen: React.FC = () => {
|
||||
const onSearchChange = useCallback((value: string) => {
|
||||
setSearchValue(value);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<CountryPickerUI
|
||||
insets={insets}
|
||||
isLoading={loading}
|
||||
countries={countryList}
|
||||
onCountrySelect={onCountrySelect}
|
||||
@@ -69,11 +72,11 @@ const CountryPickerScreen: React.FC = () => {
|
||||
getCountryName={getCountryName}
|
||||
searchValue={searchValue}
|
||||
onClose={selfClient.goBack}
|
||||
onInfoPress={() => console.log('Info pressed TODO: Implement')}
|
||||
onInfoPress={() => selfClient.trackEvent(DocumentEvents.COUNTRY_HELP_TAPPED)}
|
||||
onSearchChange={onSearchChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
CountryPickerScreen.displayName = 'CountryPickerScreen';
|
||||
|
||||
CountryPickerScreen.statusBar = CountryPickerUI.statusBar;
|
||||
export default CountryPickerScreen;
|
||||
|
||||
@@ -46,12 +46,15 @@ target "SelfDemoApp" do
|
||||
nfc_repo_url = if !is_selfxyz_repo
|
||||
puts "📦 Using public NFCPassportReader for external fork (#{ENV["GITHUB_REPOSITORY"]})"
|
||||
"https://github.com/PLACEHOLDER/NFCPassportReader.git"
|
||||
elsif ENV["GITHUB_ACTIONS"] == "true" && ENV["SELFXYZ_INTERNAL_REPO_PAT"] && !ENV["SELFXYZ_INTERNAL_REPO_PAT"].empty?
|
||||
puts "📦 Using private NFCPassportReader with PAT (selfxyz GitHub Actions)"
|
||||
"https://#{ENV["SELFXYZ_INTERNAL_REPO_PAT"]}@github.com/selfxyz/NFCPassportReader.git"
|
||||
elsif ENV["GITHUB_ACTIONS"] == "true"
|
||||
# CI: NEVER embed credentials in URLs. Rely on workflow-provided auth via:
|
||||
# - ~/.netrc or a Git credential helper, and token masking in logs.
|
||||
"https://github.com/selfxyz/NFCPassportReader.git"
|
||||
elsif using_https_git_auth?
|
||||
# Local development with HTTPS GitHub auth via gh - use HTTPS to private repo
|
||||
"https://github.com/selfxyz/NFCPassportReader.git"
|
||||
else
|
||||
# Local development in selfxyz repo - use SSH to private repo
|
||||
puts "📦 Using SSH for private NFCPassportReader (local selfxyz development)"
|
||||
"git@github.com:selfxyz/NFCPassportReader.git"
|
||||
end
|
||||
|
||||
@@ -5,7 +5,7 @@ import ScreenLayout from '../components/ScreenLayout';
|
||||
export default function CountrySelection({ onBack }: { onBack: () => void }) {
|
||||
return (
|
||||
<ScreenLayout title="GETTING STARTED" onBack={onBack}>
|
||||
<SDKCountryPickerScreen />
|
||||
<SDKCountryPickerScreen insets={{ top: 0, bottom: 0 }} />
|
||||
</ScreenLayout>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user