Merge pull request #1494 from selfxyz/release/staging-2025-12-12

Release to Staging - 2025-12-12
This commit is contained in:
Justin Hernandez
2025-12-13 23:21:54 -08:00
committed by GitHub
95 changed files with 9093 additions and 534 deletions

View 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"

View File

@@ -5,6 +5,7 @@ env:
JAVA_VERSION: 17 JAVA_VERSION: 17
WORKSPACE: ${{ github.workspace }} WORKSPACE: ${{ github.workspace }}
APP_PATH: ${{ github.workspace }}/app APP_PATH: ${{ github.workspace }}/app
NODE_ENV: "production"
on: on:
pull_request: pull_request:
@@ -57,6 +58,14 @@ jobs:
path: | path: |
~/.gradle/caches ~/.gradle/caches
~/.gradle/wrapper ~/.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 - name: Install Mobile Dependencies
uses: ./.github/actions/mobile-setup uses: ./.github/actions/mobile-setup
with: with:
@@ -65,7 +74,7 @@ jobs:
ruby_version: ${{ env.RUBY_VERSION }} ruby_version: ${{ env.RUBY_VERSION }}
workspace: ${{ env.WORKSPACE }} workspace: ${{ env.WORKSPACE }}
env: env:
SELFXYZ_INTERNAL_REPO_PAT: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }} SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }}
- name: Build dependencies - name: Build dependencies
shell: bash shell: bash
run: yarn workspace @selfxyz/common build run: yarn workspace @selfxyz/common build
@@ -113,6 +122,14 @@ jobs:
with: with:
path: app/ios/Pods path: app/ios/Pods
lockfile: app/ios/Podfile.lock 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 - name: Install Mobile Dependencies
uses: ./.github/actions/mobile-setup uses: ./.github/actions/mobile-setup
with: with:
@@ -121,7 +138,7 @@ jobs:
ruby_version: ${{ env.RUBY_VERSION }} ruby_version: ${{ env.RUBY_VERSION }}
workspace: ${{ env.WORKSPACE }} workspace: ${{ env.WORKSPACE }}
env: env:
SELFXYZ_INTERNAL_REPO_PAT: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }} SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }}
- name: Build dependencies - name: Build dependencies
shell: bash shell: bash
run: yarn workspace @selfxyz/common build run: yarn workspace @selfxyz/common build

View File

@@ -35,7 +35,7 @@ concurrency:
jobs: jobs:
build-deps: build-deps:
runs-on: macos-latest-large runs-on: ubuntu-latest
timeout-minutes: 60 timeout-minutes: 60
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@@ -90,12 +90,9 @@ jobs:
- name: Check App Types - name: Check App Types
run: yarn types run: yarn types
working-directory: ./app working-directory: ./app
- name: Check license headers
run: node scripts/check-license-headers.mjs --check
working-directory: ./
test: test:
runs-on: macos-latest-large runs-on: ubuntu-latest
needs: build-deps needs: build-deps
timeout-minutes: 60 timeout-minutes: 60
steps: steps:
@@ -190,6 +187,8 @@ jobs:
env: env:
# Increase Node.js memory to prevent hermes-parser WASM memory errors # Increase Node.js memory to prevent hermes-parser WASM memory errors
NODE_OPTIONS: --max-old-space-size=4096 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: | run: |
# Final verification from app directory perspective # Final verification from app directory perspective
echo "Final verification before running tests (from app directory)..." echo "Final verification before running tests (from app directory)..."
@@ -268,6 +267,7 @@ jobs:
- name: Cache Ruby gems - name: Cache Ruby gems
uses: ./.github/actions/cache-bundler uses: ./.github/actions/cache-bundler
with: with:
# TODO(jcortejoso): Confirm the path of the bundle cache
path: app/ios/vendor/bundle path: app/ios/vendor/bundle
lock-file: app/Gemfile.lock lock-file: app/Gemfile.lock
cache-version: ${{ env.GH_CACHE_VERSION }}-${{ env.GH_GEMS_CACHE_VERSION }}-ruby${{ env.RUBY_VERSION }} 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 config set --local path 'vendor/bundle'
bundle install --jobs 4 --retry 3 bundle install --jobs 4 --retry 3
working-directory: ./app 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 - name: Install iOS Dependencies
uses: nick-fields/retry@v3 uses: nick-fields/retry@v3
with: with:
@@ -325,7 +333,7 @@ jobs:
cd app/ios cd app/ios
bundle exec bash scripts/pod-install-with-cache-fix.sh bundle exec bash scripts/pod-install-with-cache-fix.sh
env: env:
SELFXYZ_INTERNAL_REPO_PAT: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }} SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }}
- name: Resolve iOS workspace - name: Resolve iOS workspace
run: | run: |
WORKSPACE_OPEN="ios/OpenPassport.xcworkspace" WORKSPACE_OPEN="ios/OpenPassport.xcworkspace"
@@ -470,12 +478,19 @@ jobs:
run: | run: |
echo "Cache miss for built dependencies. Building now..." echo "Cache miss for built dependencies. Building now..."
yarn workspace @selfxyz/mobile-app run build:deps 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 - name: Setup Android private modules
run: | run: |
cd ${{ env.APP_PATH }} cd ${{ env.APP_PATH }}
PLATFORM=android node scripts/setup-private-modules.cjs PLATFORM=android node scripts/setup-private-modules.cjs
env: env:
SELFXYZ_INTERNAL_REPO_PAT: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }} SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }}
CI: true CI: true
- name: Build Android (with AAPT2 symlink fix) - name: Build Android (with AAPT2 symlink fix)
run: yarn android:ci run: yarn android:ci

View File

@@ -31,6 +31,7 @@ name: Mobile Deploy
env: env:
# Build environment versions # Build environment versions
RUBY_VERSION: 3.2 RUBY_VERSION: 3.2
NODE_ENV: "production"
JAVA_VERSION: 17 JAVA_VERSION: 17
ANDROID_API_LEVEL: 35 ANDROID_API_LEVEL: 35
ANDROID_NDK_VERSION: 27.0.12077973 ANDROID_NDK_VERSION: 27.0.12077973
@@ -385,6 +386,7 @@ jobs:
id: gems-cache id: gems-cache
uses: ./.github/actions/cache-bundler uses: ./.github/actions/cache-bundler
with: with:
# TODO(jcortejoso): Confirm the path of the bundle cache
path: ${{ env.APP_PATH }}/ios/vendor/bundle path: ${{ env.APP_PATH }}/ios/vendor/bundle
lock-file: app/Gemfile.lock lock-file: app/Gemfile.lock
cache-version: ${{ env.GH_CACHE_VERSION }}-${{ env.GH_GEMS_CACHE_VERSION }}-ruby${{ env.RUBY_VERSION }} cache-version: ${{ env.GH_CACHE_VERSION }}-${{ env.GH_GEMS_CACHE_VERSION }}-ruby${{ env.RUBY_VERSION }}
@@ -428,6 +430,14 @@ jobs:
fi fi
echo "✅ Lock files exist" 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) - name: Install Mobile Dependencies (main repo)
if: inputs.platform != 'android' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) 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 }} ruby_version: ${{ env.RUBY_VERSION }}
workspace: ${{ env.WORKSPACE }} workspace: ${{ env.WORKSPACE }}
env: 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) - 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 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 }} IOS_TESTFLIGHT_GROUPS: ${{ secrets.IOS_TESTFLIGHT_GROUPS }}
NODE_OPTIONS: "--max-old-space-size=8192" NODE_OPTIONS: "--max-old-space-size=8192"
SEGMENT_KEY: ${{ secrets.SEGMENT_KEY }} 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 }} SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
TURNKEY_AUTH_PROXY_CONFIG_ID: ${{ secrets.TURNKEY_AUTH_PROXY_CONFIG_ID }} TURNKEY_AUTH_PROXY_CONFIG_ID: ${{ secrets.TURNKEY_AUTH_PROXY_CONFIG_ID }}
TURNKEY_GOOGLE_CLIENT_ID: ${{ secrets.TURNKEY_GOOGLE_CLIENT_ID }} TURNKEY_GOOGLE_CLIENT_ID: ${{ secrets.TURNKEY_GOOGLE_CLIENT_ID }}
@@ -1046,6 +1056,14 @@ jobs:
echo "✅ Lock files exist" 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) - name: Install Mobile Dependencies (main repo)
if: inputs.platform != 'ios' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) if: inputs.platform != 'ios' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
uses: ./.github/actions/mobile-setup uses: ./.github/actions/mobile-setup
@@ -1055,7 +1073,7 @@ jobs:
ruby_version: ${{ env.RUBY_VERSION }} ruby_version: ${{ env.RUBY_VERSION }}
workspace: ${{ env.WORKSPACE }} workspace: ${{ env.WORKSPACE }}
env: env:
SELFXYZ_INTERNAL_REPO_PAT: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }} SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }}
PLATFORM: ${{ inputs.platform }} PLATFORM: ${{ inputs.platform }}
- name: Install Mobile Dependencies (forked PRs - no secrets) - name: Install Mobile Dependencies (forked PRs - no secrets)
@@ -1112,7 +1130,7 @@ jobs:
cd ${{ env.APP_PATH }} cd ${{ env.APP_PATH }}
PLATFORM=android node scripts/setup-private-modules.cjs PLATFORM=android node scripts/setup-private-modules.cjs
env: env:
SELFXYZ_INTERNAL_REPO_PAT: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }} SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }}
CI: true CI: true
- name: Build Dependencies (Android) - name: Build Dependencies (Android)

View File

@@ -70,6 +70,14 @@ jobs:
- name: Toggle Yarn hardened mode for trusted PRs - name: Toggle Yarn hardened mode for trusted PRs
if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false }} if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false }}
run: echo "YARN_ENABLE_HARDENED_MODE=0" >> $GITHUB_ENV 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) - name: Install deps (internal PRs and protected branches)
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }} if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
uses: nick-fields/retry@v3 uses: nick-fields/retry@v3
@@ -79,7 +87,7 @@ jobs:
retry_wait_seconds: 5 retry_wait_seconds: 5
command: yarn install --immutable --silent command: yarn install --immutable --silent
env: 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) - name: Install deps (forked PRs - no secrets)
if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true }} if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true }}
uses: nick-fields/retry@v3 uses: nick-fields/retry@v3
@@ -138,7 +146,7 @@ jobs:
cd app cd app
PLATFORM=android node scripts/setup-private-modules.cjs PLATFORM=android node scripts/setup-private-modules.cjs
env: env:
SELFXYZ_INTERNAL_REPO_PAT: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }} SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }}
CI: true CI: true
- name: Build Android APK - name: Build Android APK
run: | run: |
@@ -149,6 +157,8 @@ jobs:
- name: Clean up Gradle build artifacts - name: Clean up Gradle build artifacts
uses: ./.github/actions/cleanup-gradle-artifacts uses: ./.github/actions/cleanup-gradle-artifacts
- name: Verify APK and android-passport-nfc-reader integration - name: Verify APK and android-passport-nfc-reader integration
env:
SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }}
run: | run: |
echo "🔍 Verifying build artifacts..." echo "🔍 Verifying build artifacts..."
APK_PATH="app/android/app/build/outputs/apk/debug/app-debug.apk" APK_PATH="app/android/app/build/outputs/apk/debug/app-debug.apk"
@@ -160,8 +170,8 @@ jobs:
echo "📱 APK size: $APK_SIZE bytes" echo "📱 APK size: $APK_SIZE bytes"
# Verify private modules were properly integrated (skip for forks) # Verify private modules were properly integrated (skip for forks)
if [ -z "${SELFXYZ_INTERNAL_REPO_PAT:-}" ]; then if [ -z "${SELFXYZ_APP_TOKEN:-}" ]; then
echo "🔕 No PAT available — skipping private module verification" echo "🔕 No SELFXYZ_APP_TOKEN available — skipping private module verification"
else else
# Verify android-passport-nfc-reader # Verify android-passport-nfc-reader
if [ -d "app/android/android-passport-nfc-reader" ]; then if [ -d "app/android/android-passport-nfc-reader" ]; then
@@ -263,6 +273,14 @@ jobs:
- name: Toggle Yarn hardened mode for trusted PRs - name: Toggle Yarn hardened mode for trusted PRs
if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false }} if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false }}
run: echo "YARN_ENABLE_HARDENED_MODE=0" >> $GITHUB_ENV 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) - name: Install deps (internal PRs and protected branches)
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }} if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
uses: nick-fields/retry@v3 uses: nick-fields/retry@v3
@@ -272,7 +290,7 @@ jobs:
retry_wait_seconds: 5 retry_wait_seconds: 5
command: yarn install --immutable --silent command: yarn install --immutable --silent
env: 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) - name: Install deps (forked PRs - no secrets)
if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true }} if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true }}
uses: nick-fields/retry@v3 uses: nick-fields/retry@v3
@@ -360,7 +378,7 @@ jobs:
echo "📦 Installing pods via centralized script…" echo "📦 Installing pods via centralized script…"
BUNDLE_GEMFILE=../Gemfile bundle exec bash scripts/pod-install-with-cache-fix.sh BUNDLE_GEMFILE=../Gemfile bundle exec bash scripts/pod-install-with-cache-fix.sh
env: env:
SELFXYZ_INTERNAL_REPO_PAT: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }} SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }}
- name: Setup iOS Simulator - name: Setup iOS Simulator
run: | run: |
echo "Setting up iOS Simulator..." echo "Setting up iOS Simulator..."

View File

@@ -59,6 +59,9 @@ jobs:
node-version: ${{ env.NODE_VERSION }} node-version: ${{ env.NODE_VERSION }}
- run: corepack enable - run: corepack enable
- run: corepack prepare yarn@4.6.0 --activate - 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 - name: Cache Yarn dependencies
uses: ./.github/actions/cache-yarn uses: ./.github/actions/cache-yarn
with: with:
@@ -66,10 +69,17 @@ jobs:
.yarn/cache .yarn/cache
.yarn/install-state.gz .yarn/install-state.gz
.yarn/unplugged .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 - name: Toggle Yarn hardened mode for trusted PRs
if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false }} if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false }}
run: echo "YARN_ENABLE_HARDENED_MODE=0" >> $GITHUB_ENV 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) - name: Install deps (internal PRs and protected branches)
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }} if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
uses: nick-fields/retry@v3 uses: nick-fields/retry@v3
@@ -79,7 +89,7 @@ jobs:
retry_wait_seconds: 5 retry_wait_seconds: 5
command: yarn install --immutable --silent command: yarn install --immutable --silent
env: 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) - name: Install deps (forked PRs - no secrets)
if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true }} if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true }}
uses: nick-fields/retry@v3 uses: nick-fields/retry@v3
@@ -220,6 +230,9 @@ jobs:
node-version: ${{ env.NODE_VERSION }} node-version: ${{ env.NODE_VERSION }}
- run: corepack enable - run: corepack enable
- run: corepack prepare yarn@4.6.0 --activate - 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 - name: Cache Yarn dependencies
uses: ./.github/actions/cache-yarn uses: ./.github/actions/cache-yarn
with: with:
@@ -227,10 +240,17 @@ jobs:
.yarn/cache .yarn/cache
.yarn/install-state.gz .yarn/install-state.gz
.yarn/unplugged .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 - name: Toggle Yarn hardened mode for trusted PRs
if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false }} if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false }}
run: echo "YARN_ENABLE_HARDENED_MODE=0" >> $GITHUB_ENV 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) - name: Install deps (internal PRs and protected branches)
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }} if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false }}
uses: nick-fields/retry@v3 uses: nick-fields/retry@v3
@@ -240,7 +260,7 @@ jobs:
retry_wait_seconds: 5 retry_wait_seconds: 5
command: yarn install --immutable --silent command: yarn install --immutable --silent
env: 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) - name: Install deps (forked PRs - no secrets)
if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true }} if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true }}
uses: nick-fields/retry@v3 uses: nick-fields/retry@v3
@@ -316,15 +336,15 @@ jobs:
max_attempts: 3 max_attempts: 3
retry_wait_seconds: 10 retry_wait_seconds: 10
command: | command: |
if [ -n "${SELFXYZ_INTERNAL_REPO_PAT}" ]; then if [ -n "${SELFXYZ_APP_TOKEN}" ]; then
echo "🔑 Using SELFXYZ_INTERNAL_REPO_PAT for private pod access" echo "🔑 Using GitHub App token for private pod access"
echo "::add-mask::${SELFXYZ_INTERNAL_REPO_PAT}" echo "::add-mask::${SELFXYZ_APP_TOKEN}"
fi fi
cd packages/mobile-sdk-demo/ios cd packages/mobile-sdk-demo/ios
echo "📦 Installing pods via cache-fix script…" echo "📦 Installing pods via cache-fix script…"
bash scripts/pod-install-with-cache-fix.sh bash scripts/pod-install-with-cache-fix.sh
env: env:
SELFXYZ_INTERNAL_REPO_PAT: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }} SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }}
GIT_TERMINAL_PROMPT: 0 GIT_TERMINAL_PROMPT: 0
- name: Setup iOS Simulator - name: Setup iOS Simulator
run: | run: |

View File

@@ -76,6 +76,7 @@ jobs:
with: with:
path: | path: |
common/dist common/dist
sdk/sdk-common/dist
sdk/qrcode/dist sdk/qrcode/dist
key: qrcode-sdk-build-${{ env.GH_SDK_CACHE_VERSION }}-${{ github.sha }} key: qrcode-sdk-build-${{ env.GH_SDK_CACHE_VERSION }}-${{ github.sha }}
@@ -128,6 +129,7 @@ jobs:
with: with:
path: | path: |
common/dist common/dist
sdk/sdk-common/dist
sdk/qrcode/dist sdk/qrcode/dist
key: qrcode-sdk-build-${{ env.GH_SDK_CACHE_VERSION }}-${{ github.sha }} key: qrcode-sdk-build-${{ env.GH_SDK_CACHE_VERSION }}-${{ github.sha }}
fail-on-cache-miss: true fail-on-cache-miss: true
@@ -195,6 +197,7 @@ jobs:
with: with:
path: | path: |
common/dist common/dist
sdk/sdk-common/dist
sdk/qrcode/dist sdk/qrcode/dist
key: qrcode-sdk-build-${{ env.GH_SDK_CACHE_VERSION }}-${{ github.sha }} key: qrcode-sdk-build-${{ env.GH_SDK_CACHE_VERSION }}-${{ github.sha }}
fail-on-cache-miss: true fail-on-cache-miss: true
@@ -203,6 +206,7 @@ jobs:
run: | run: |
echo "Verifying build artifacts..." echo "Verifying build artifacts..."
ls -la common/dist/ ls -la common/dist/
ls -la sdk/sdk-common/dist/
ls -la sdk/qrcode/dist/ ls -la sdk/qrcode/dist/
echo "✅ Build artifacts verified" echo "✅ Build artifacts verified"
@@ -255,13 +259,11 @@ jobs:
with: with:
path: | path: |
common/dist common/dist
sdk/sdk-common/dist
sdk/qrcode/dist sdk/qrcode/dist
key: qrcode-sdk-build-${{ env.GH_SDK_CACHE_VERSION }}-${{ github.sha }} key: qrcode-sdk-build-${{ env.GH_SDK_CACHE_VERSION }}-${{ github.sha }}
fail-on-cache-miss: true fail-on-cache-miss: true
- name: Build SDK common dependency
run: yarn workspace @selfxyz/sdk-common build
- name: Run tests - name: Run tests
run: yarn workspace @selfxyz/qrcode test run: yarn workspace @selfxyz/qrcode test

5
.gitignore vendored
View File

@@ -24,3 +24,8 @@ packages/mobile-sdk-alpha/docs/docstrings-report.json
# Private Android modules (cloned at build time) # Private Android modules (cloned at build time)
app/android/android-passport-nfc-reader/ app/android/android-passport-nfc-reader/
# Foundry
contracts/out/
contracts/cache_forge/
contracts/broadcast/

7
.gitmodules vendored
View File

@@ -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"] [submodule "packages/mobile-sdk-alpha/mobile-sdk-native"]
path = packages/mobile-sdk-alpha/mobile-sdk-native path = packages/mobile-sdk-alpha/mobile-sdk-native
url = git@github.com:selfxyz/mobile-sdk-native.git url = git@github.com:selfxyz/mobile-sdk-native.git

View File

@@ -245,6 +245,8 @@ module.exports = {
], ],
// Allow any types in tests for mocking // Allow any types in tests for mocking
'@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-explicit-any': 'off',
// Allow test skipping without warnings
'jest/no-disabled-tests': 'off',
}, },
}, },
{ {

View File

@@ -22,7 +22,7 @@ GEM
artifactory (3.0.17) artifactory (3.0.17)
atomos (0.1.3) atomos (0.1.3)
aws-eventstream (1.4.0) aws-eventstream (1.4.0)
aws-partitions (1.1190.0) aws-partitions (1.1194.0)
aws-sdk-core (3.239.2) aws-sdk-core (3.239.2)
aws-eventstream (~> 1, >= 1.3.0) aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0) aws-partitions (~> 1, >= 1.992.0)
@@ -86,7 +86,7 @@ GEM
colored2 (3.1.2) colored2 (3.1.2)
commander (4.6.0) commander (4.6.0)
highline (~> 2.0.0) highline (~> 2.0.0)
concurrent-ruby (1.3.5) concurrent-ruby (1.3.6)
connection_pool (3.0.2) connection_pool (3.0.2)
declarative (0.0.20) declarative (0.0.20)
digest-crc (0.7.0) digest-crc (0.7.0)
@@ -222,14 +222,14 @@ GEM
i18n (1.14.7) i18n (1.14.7)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
jmespath (1.6.2) jmespath (1.6.2)
json (2.17.1) json (2.18.0)
jwt (2.10.2) jwt (2.10.2)
base64 base64
logger (1.7.0) logger (1.7.0)
mini_magick (4.13.2) mini_magick (4.13.2)
mini_mime (1.1.5) mini_mime (1.1.5)
mini_portile2 (2.8.9) mini_portile2 (2.8.9)
minitest (5.26.2) minitest (5.27.0)
molinillo (0.8.0) molinillo (0.8.0)
multi_json (1.18.0) multi_json (1.18.0)
multipart-post (2.4.1) multipart-post (2.4.1)
@@ -241,7 +241,7 @@ GEM
nokogiri (1.18.10) nokogiri (1.18.10)
mini_portile2 (~> 2.8.2) mini_portile2 (~> 2.8.2)
racc (~> 1.4) racc (~> 1.4)
optparse (0.8.0) optparse (0.8.1)
os (1.1.4) os (1.1.4)
plist (3.7.2) plist (3.7.2)
public_suffix (4.0.7) public_suffix (4.0.7)

View File

@@ -26,4 +26,9 @@ module.exports = {
}, },
], ],
], ],
env: {
production: {
plugins: ['transform-remove-console'],
},
},
}; };

View File

@@ -42,8 +42,15 @@
<string></string> <string></string>
<key>LSApplicationQueriesSchemes</key> <key>LSApplicationQueriesSchemes</key>
<array> <array>
<string>whatsapp</string> <string>argent</string>
<string>cbwallet</string>
<string>coinbase</string>
<string>metamask</string>
<string>rainbow</string>
<string>sms</string> <string>sms</string>
<string>trust</string>
<string>wc</string>
<string>whatsapp</string>
</array> </array>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <true/>

View File

@@ -33,7 +33,7 @@ def using_https_git_auth?
auth_data.include?("Logged in to github.com account") && auth_data.include?("Logged in to github.com account") &&
auth_data.include?("Git operations protocol: https") auth_data.include?("Git operations protocol: https")
rescue => e rescue => e
puts "gh auth status failed, assuming no HTTPS auth -- will try SSH" # Avoid printing auth-related details in CI logs.
false false
end end
end end
@@ -51,18 +51,16 @@ target "Self" do
# External fork - use public NFCPassportReader repository (placeholder) # External fork - use public NFCPassportReader repository (placeholder)
# TODO: Replace with actual public NFCPassportReader repository URL # TODO: Replace with actual public NFCPassportReader repository URL
nfc_repo_url = "https://github.com/PLACEHOLDER/NFCPassportReader.git" nfc_repo_url = "https://github.com/PLACEHOLDER/NFCPassportReader.git"
puts "📦 Using public NFCPassportReader for external fork (#{ENV["GITHUB_REPOSITORY"]})" elsif ENV["GITHUB_ACTIONS"] == "true"
elsif ENV["GITHUB_ACTIONS"] == "true" && ENV["SELFXYZ_INTERNAL_REPO_PAT"] # CI: NEVER embed credentials in URLs. Rely on workflow-provided auth via:
# Running in selfxyz GitHub Actions with PAT available - use private repo with token # - ~/.netrc or a Git credential helper, and token masking in logs.
nfc_repo_url = "https://#{ENV["SELFXYZ_INTERNAL_REPO_PAT"]}@github.com/selfxyz/NFCPassportReader.git" nfc_repo_url = "https://github.com/selfxyz/NFCPassportReader.git"
puts "📦 Using private NFCPassportReader with PAT (selfxyz GitHub Actions)"
elsif using_https_git_auth? elsif using_https_git_auth?
# Local development with HTTPS GitHub auth via gh - use HTTPS to private repo # Local development with HTTPS GitHub auth via gh - use HTTPS to private repo
nfc_repo_url = "https://github.com/selfxyz/NFCPassportReader.git" nfc_repo_url = "https://github.com/selfxyz/NFCPassportReader.git"
else else
# Local development in selfxyz repo - use SSH to private repo # Local development in selfxyz repo - use SSH to private repo
nfc_repo_url = "git@github.com:selfxyz/NFCPassportReader.git" nfc_repo_url = "git@github.com:selfxyz/NFCPassportReader.git"
puts "📦 Using SSH for private NFCPassportReader (local selfxyz development)"
end end
pod "NFCPassportReader", git: nfc_repo_url, commit: "9eff7c4e3a9037fdc1e03301584e0d5dcf14d76b" pod "NFCPassportReader", git: nfc_repo_url, commit: "9eff7c4e3a9037fdc1e03301584e0d5dcf14d76b"

View File

@@ -2644,6 +2644,6 @@ SPEC CHECKSUMS:
SwiftyTesseract: 1f3d96668ae92dc2208d9842c8a59bea9fad2cbb SwiftyTesseract: 1f3d96668ae92dc2208d9842c8a59bea9fad2cbb
Yoga: 1259c7a8cbaccf7b4c3ddf8ee36ca11be9dee407 Yoga: 1259c7a8cbaccf7b4c3ddf8ee36ca11be9dee407
PODFILE CHECKSUM: b5f11f935be22fce84c5395aaa203b50427a79aa PODFILE CHECKSUM: 0aa47f53692543349c43673cda7380fa23049eba
COCOAPODS: 1.16.2 COCOAPODS: 1.16.2

View File

@@ -16,7 +16,7 @@ module.exports = {
'node', 'node',
], ],
transformIgnorePatterns: [ 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'], setupFiles: ['<rootDir>/jest.setup.js'],
testMatch: [ testMatch: [

View File

@@ -58,7 +58,7 @@
"sync-versions": "bundle exec fastlane ios sync_version && bundle exec fastlane android sync_version", "sync-versions": "bundle exec fastlane ios sync_version && bundle exec fastlane android sync_version",
"tag:release": "node scripts/tag.cjs release", "tag:release": "node scripts/tag.cjs release",
"tag:remove": "node scripts/tag.cjs remove", "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: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:ci": "yarn jest:run --passWithNoTests && node --test scripts/tests/*.cjs",
"test:coverage": "yarn jest:run --coverage --passWithNoTests", "test:coverage": "yarn jest:run --coverage --passWithNoTests",
@@ -105,6 +105,7 @@
"@segment/analytics-react-native": "^2.21.2", "@segment/analytics-react-native": "^2.21.2",
"@segment/sovran-react-native": "^1.1.3", "@segment/sovran-react-native": "^1.1.3",
"@selfxyz/common": "workspace:^", "@selfxyz/common": "workspace:^",
"@selfxyz/euclid": "^0.6.0",
"@selfxyz/mobile-sdk-alpha": "workspace:^", "@selfxyz/mobile-sdk-alpha": "workspace:^",
"@sentry/react": "^9.32.0", "@sentry/react": "^9.32.0",
"@sentry/react-native": "7.0.1", "@sentry/react-native": "7.0.1",
@@ -209,6 +210,7 @@
"@typescript-eslint/parser": "^8.39.0", "@typescript-eslint/parser": "^8.39.0",
"@vitejs/plugin-react-swc": "^3.10.2", "@vitejs/plugin-react-swc": "^3.10.2",
"babel-plugin-module-resolver": "^5.0.2", "babel-plugin-module-resolver": "^5.0.2",
"babel-plugin-transform-remove-console": "^6.9.4",
"constants-browserify": "^1.0.0", "constants-browserify": "^1.0.0",
"dompurify": "^3.2.6", "dompurify": "^3.2.6",
"eslint": "^8.57.0", "eslint": "^8.57.0",

View File

@@ -17,8 +17,8 @@ if (!platform || !['android', 'ios'].includes(platform)) {
// Bundle size thresholds in MB - easy to update! // Bundle size thresholds in MB - easy to update!
const BUNDLE_THRESHOLDS_MB = { const BUNDLE_THRESHOLDS_MB = {
// TODO: fix temporary bundle bump // TODO: fix temporary bundle bump
ios: 44, ios: 45,
android: 44, android: 45,
}; };
function formatBytes(bytes) { function formatBytes(bytes) {

View File

@@ -109,7 +109,13 @@ clone_private_module() {
local dir_name=$(basename "$target_dir") local dir_name=$(basename "$target_dir")
# Use different clone methods based on environment # 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) # 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" || { git clone "https://${SELFXYZ_INTERNAL_REPO_PAT}@github.com/selfxyz/${repo_name}.git" "$dir_name" || {
log "ERROR: Failed to clone $repo_name with PAT" log "ERROR: Failed to clone $repo_name with PAT"
@@ -119,14 +125,14 @@ clone_private_module() {
# Local development with SSH # Local development with SSH
git clone "git@github.com:selfxyz/${repo_name}.git" "$dir_name" || { git clone "git@github.com:selfxyz/${repo_name}.git" "$dir_name" || {
log "ERROR: Failed to clone $repo_name with SSH" 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 exit 1
} }
else else
log "ERROR: No authentication method available for cloning $repo_name" log "ERROR: No authentication method available for cloning $repo_name"
log "Please either:" log "Please either:"
log " - Set up SSH access (for local development)" 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 exit 1
fi fi
@@ -194,14 +200,15 @@ log "✅ Package files backed up successfully"
# Install SDK from tarball in app with timeout # Install SDK from tarball in app with timeout
log "Installing SDK as real files..." log "Installing SDK as real files..."
if is_ci; then if is_ci; then
# Temporarily unset PAT to skip private modules during SDK installation # Temporarily unset both auth tokens to skip private modules during SDK installation
env -u SELFXYZ_INTERNAL_REPO_PAT timeout 180 yarn add "@selfxyz/mobile-sdk-alpha@file:$TARBALL_PATH" || { # 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" log "SDK installation timed out after 3 minutes"
exit 1 exit 1
} }
else else
# Temporarily unset PAT to skip private modules during SDK installation # Temporarily unset both auth tokens to skip private modules during SDK installation
env -u SELFXYZ_INTERNAL_REPO_PAT yarn add "@selfxyz/mobile-sdk-alpha@file:$TARBALL_PATH" env -u SELFXYZ_INTERNAL_REPO_PAT -u SELFXYZ_APP_TOKEN yarn add "@selfxyz/mobile-sdk-alpha@file:$TARBALL_PATH"
fi fi
# Verify installation (check for AAR file in both local and hoisted locations) # Verify installation (check for AAR file in both local and hoisted locations)

View File

@@ -29,8 +29,9 @@ const PRIVATE_MODULES = [
// Environment detection // Environment detection
// CI is set by GitHub Actions, CircleCI, etc. Check for truthy value // 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 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'; const isDryRun = process.env.DRY_RUN === 'true';
// Platform detection for Android-specific modules // Platform detection for Android-specific modules
@@ -150,13 +151,17 @@ function clonePrivateRepo(repoName, localPath) {
let cloneUrl; 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 // CI environment with Personal Access Token
log('CI detected: Using SELFXYZ_INTERNAL_REPO_PAT for clone', 'info'); log('CI detected: Using SELFXYZ_INTERNAL_REPO_PAT for clone', 'info');
cloneUrl = `https://${repoToken}@github.com/${GITHUB_ORG}/${repoName}.git`; cloneUrl = `https://${repoToken}@github.com/${GITHUB_ORG}/${repoName}.git`;
} else if (isCI) { } else if (isCI) {
log( 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', 'info',
); );
log( log(
@@ -173,7 +178,7 @@ function clonePrivateRepo(repoName, localPath) {
} }
// Security: Use quiet mode for credentialed URLs to prevent token exposure // 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 quietFlag = isCredentialedUrl ? '--quiet' : '';
const targetDir = path.basename(localPath); const targetDir = path.basename(localPath);
const cloneCommand = `git clone --branch ${BRANCH} --single-branch --depth 1 ${quietFlag} "${cloneUrl}" "${targetDir}"`; const cloneCommand = `git clone --branch ${BRANCH} --single-branch --depth 1 ${quietFlag} "${cloneUrl}" "${targetDir}"`;
@@ -190,7 +195,7 @@ function clonePrivateRepo(repoName, localPath) {
} catch (error) { } catch (error) {
if (isCI) { if (isCI) {
log( 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', 'error',
); );
} else { } else {
@@ -231,7 +236,7 @@ function setupPrivateModule(module) {
} }
// Security: Remove credential-embedded remote URL after clone // Security: Remove credential-embedded remote URL after clone
if (isCI && repoToken && !isDryRun) { if (isCI && (appToken || repoToken) && !isDryRun) {
scrubGitRemoteUrl(localPath, repoName); scrubGitRemoteUrl(localPath, repoName);
} }
@@ -275,6 +280,11 @@ function setupAndroidPassportReader() {
`Setup complete: ${successCount}/${PRIVATE_MODULES.length} modules cloned`, `Setup complete: ${successCount}/${PRIVATE_MODULES.length} modules cloned`,
'warning', 'warning',
); );
} else {
log(
'No private modules were cloned - this is expected for forked PRs',
'info',
);
} }
} }

View File

@@ -6,11 +6,7 @@ import React from 'react';
import { ArrowLeft, ArrowRight, RotateCcw } from '@tamagui/lucide-icons'; import { ArrowLeft, ArrowRight, RotateCcw } from '@tamagui/lucide-icons';
import { Button, XStack, YStack } from '@selfxyz/mobile-sdk-alpha/components'; import { Button, XStack, YStack } from '@selfxyz/mobile-sdk-alpha/components';
import { import { black, slate400 } from '@selfxyz/mobile-sdk-alpha/constants/colors';
black,
slate50,
slate400,
} from '@selfxyz/mobile-sdk-alpha/constants/colors';
import { buttonTap } from '@/integrations/haptics'; import { buttonTap } from '@/integrations/haptics';
@@ -23,8 +19,7 @@ export interface WebViewFooterProps {
onOpenInBrowser: () => void; onOpenInBrowser: () => void;
} }
const iconSize = 22; const iconSize = 24;
const buttonSize = 36;
export const WebViewFooter: React.FC<WebViewFooterProps> = ({ export const WebViewFooter: React.FC<WebViewFooterProps> = ({
canGoBack, canGoBack,
@@ -42,19 +37,13 @@ export const WebViewFooter: React.FC<WebViewFooterProps> = ({
) => ( ) => (
<Button <Button
key={key} key={key}
size="$4"
unstyled unstyled
disabled={disabled} disabled={disabled}
hitSlop={{ top: 20, bottom: 20, left: 20, right: 20 }}
onPress={() => { onPress={() => {
buttonTap(); buttonTap();
onPress(); onPress();
}} }}
backgroundColor={slate50}
borderRadius={buttonSize / 2}
width={buttonSize}
height={buttonSize}
alignItems="center"
justifyContent="center"
opacity={disabled ? 0.5 : 1} opacity={disabled ? 0.5 : 1}
> >
{icon} {icon}
@@ -62,7 +51,7 @@ export const WebViewFooter: React.FC<WebViewFooterProps> = ({
); );
return ( 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%"> <XStack justifyContent="space-between" alignItems="center" width="100%">
{renderIconButton( {renderIconButton(
'back', 'back',

View 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}
/>
</>
);
};

View File

@@ -30,9 +30,9 @@ export const WebViewNavBar: React.FC<WebViewNavBarProps> = ({
return ( return (
<XStack <XStack
paddingHorizontal={20}
paddingVertical={10} paddingVertical={10}
paddingTop={insets.top + 10} paddingTop={insets.top + 10}
paddingHorizontal={16}
gap={14} gap={14}
alignItems="center" alignItems="center"
backgroundColor="white" backgroundColor="white"
@@ -50,7 +50,12 @@ export const WebViewNavBar: React.FC<WebViewNavBarProps> = ({
/> />
{/* Center: Title */} {/* Center: Title */}
<XStack flex={1} alignItems="center" justifyContent="center"> <XStack
flex={1}
alignItems="center"
justifyContent="center"
paddingHorizontal={8}
>
<Text style={styles.title} numberOfLines={1}> <Text style={styles.title} numberOfLines={1}>
{title?.toUpperCase() || 'PAGE TITLE'} {title?.toUpperCase() || 'PAGE TITLE'}
</Text> </Text>

View 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),
};
}

View File

@@ -10,6 +10,7 @@ import {
white, white,
} from '@selfxyz/mobile-sdk-alpha/constants/colors'; } from '@selfxyz/mobile-sdk-alpha/constants/colors';
import { HeadlessNavForEuclid } from '@/components/navbar/HeadlessNavForEuclid';
import AccountRecoveryChoiceScreen from '@/screens/account/recovery/AccountRecoveryChoiceScreen'; import AccountRecoveryChoiceScreen from '@/screens/account/recovery/AccountRecoveryChoiceScreen';
import AccountRecoveryScreen from '@/screens/account/recovery/AccountRecoveryScreen'; import AccountRecoveryScreen from '@/screens/account/recovery/AccountRecoveryScreen';
import DocumentDataNotFoundScreen from '@/screens/account/recovery/DocumentDataNotFoundScreen'; 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 CloudBackupScreen from '@/screens/account/settings/CloudBackupScreen';
import SettingsScreen from '@/screens/account/settings/SettingsScreen'; import SettingsScreen from '@/screens/account/settings/SettingsScreen';
import ShowRecoveryPhraseScreen from '@/screens/account/settings/ShowRecoveryPhraseScreen'; import ShowRecoveryPhraseScreen from '@/screens/account/settings/ShowRecoveryPhraseScreen';
import { IS_EUCLID_ENABLED } from '@/utils/devUtils';
const accountScreens = { const accountScreens = {
AccountRecovery: { AccountRecovery: {
@@ -79,14 +81,22 @@ const accountScreens = {
screens: {}, screens: {},
}, },
}, },
ShowRecoveryPhrase: { ShowRecoveryPhrase: {
screen: ShowRecoveryPhraseScreen, screen: ShowRecoveryPhraseScreen,
options: { options: IS_EUCLID_ENABLED
title: 'Recovery Phrase', ? ({
headerStyle: { headerShown: true,
backgroundColor: white, header: HeadlessNavForEuclid,
}, statusBarStyle: ShowRecoveryPhraseScreen.statusBarStyle,
} as NativeStackNavigationOptions, statusBarHidden: ShowRecoveryPhraseScreen.statusBarHidden,
} as NativeStackNavigationOptions)
: ({
title: 'Recovery Phrase',
headerStyle: {
backgroundColor: white,
},
} as NativeStackNavigationOptions),
}, },
}; };

View File

@@ -7,6 +7,7 @@ import type { NativeStackNavigationOptions } from '@react-navigation/native-stac
import { black, white } from '@selfxyz/mobile-sdk-alpha/constants/colors'; import { black, white } from '@selfxyz/mobile-sdk-alpha/constants/colors';
import { AadhaarNavBar, IdDetailsNavBar } from '@/components/navbar'; import { AadhaarNavBar, IdDetailsNavBar } from '@/components/navbar';
import { HeadlessNavForEuclid } from '@/components/navbar/HeadlessNavForEuclid';
import AadhaarUploadedSuccessScreen from '@/screens/documents/aadhaar/AadhaarUploadedSuccessScreen'; import AadhaarUploadedSuccessScreen from '@/screens/documents/aadhaar/AadhaarUploadedSuccessScreen';
import AadhaarUploadErrorScreen from '@/screens/documents/aadhaar/AadhaarUploadErrorScreen'; import AadhaarUploadErrorScreen from '@/screens/documents/aadhaar/AadhaarUploadErrorScreen';
import AadhaarUploadScreen from '@/screens/documents/aadhaar/AadhaarUploadScreen'; import AadhaarUploadScreen from '@/screens/documents/aadhaar/AadhaarUploadScreen';
@@ -76,7 +77,10 @@ const documentsScreens = {
CountryPicker: { CountryPicker: {
screen: CountryPickerScreen, screen: CountryPickerScreen,
options: { options: {
headerShown: false, header: HeadlessNavForEuclid,
statusBarHidden: CountryPickerScreen.statusBar?.hidden,
statusBarStyle: CountryPickerScreen.statusBar?.style,
headerShown: true,
} as NativeStackNavigationOptions, } as NativeStackNavigationOptions,
}, },
IDPicker: { IDPicker: {

View File

@@ -2,21 +2,85 @@
// SPDX-License-Identifier: BUSL-1.1 // SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. // 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 { Description } from '@selfxyz/mobile-sdk-alpha/components';
import Mnemonic from '@/components/Mnemonic'; import Mnemonic from '@/components/Mnemonic';
import useMnemonic from '@/hooks/useMnemonic'; import useMnemonic from '@/hooks/useMnemonic';
import { useSafeAreaInsets } from '@/hooks/useSafeAreaInsets';
import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout'; 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 { mnemonic, loadMnemonic } = useMnemonic();
const self = useSelfClient();
const { copied, onCopy } = useCopyRecoveryPhrase(mnemonic);
const { setHasViewedRecoveryPhrase } = useSettingStore();
const onRevealWords = useCallback(async () => { const onReveal = useCallback(async () => {
await loadMnemonic(); 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 ( return (
<ExpandableBottomLayout.Layout backgroundColor="white"> <ExpandableBottomLayout.Layout backgroundColor="white">
<ExpandableBottomLayout.BottomSection <ExpandableBottomLayout.BottomSection
@@ -24,7 +88,7 @@ const ShowRecoveryPhraseScreen: React.FC = () => {
justifyContent="center" justifyContent="center"
gap={20} gap={20}
> >
<Mnemonic words={mnemonic} onRevealWords={onRevealWords} /> <Mnemonic words={mnemonic} onRevealWords={loadMnemonic} />
<Description> <Description>
This phrase is the only way to recover your account. Keep it secret, This phrase is the only way to recover your account. Keep it secret,
keep it safe. keep it safe.
@@ -35,3 +99,7 @@ const ShowRecoveryPhraseScreen: React.FC = () => {
}; };
export default ShowRecoveryPhraseScreen; export default ShowRecoveryPhraseScreen;
ShowRecoveryPhraseScreen.statusBarHidden =
RecoveryPhraseScreen.statusBar.hidden;
ShowRecoveryPhraseScreen.statusBarStyle = RecoveryPhraseScreen.statusBar.style;

View File

@@ -2,8 +2,21 @@
// SPDX-License-Identifier: BUSL-1.1 // SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. // 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'; import SDKCountryPickerScreen from '@selfxyz/mobile-sdk-alpha/onboarding/country-picker-screen';
export default function CountryPickerScreen() { import { useSafeAreaInsets } from '@/hooks/useSafeAreaInsets';
return <SDKCountryPickerScreen />;
} 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;

View File

@@ -5,8 +5,10 @@
import React, { useCallback, useMemo, useRef, useState } from 'react'; import React, { useCallback, useMemo, useRef, useState } from 'react';
import { import {
ActivityIndicator, ActivityIndicator,
Alert,
BackHandler, BackHandler,
Linking, Linking,
Platform,
StyleSheet, StyleSheet,
View, View,
} from 'react-native'; } from 'react-native';
@@ -26,6 +28,14 @@ import { WebViewFooter } from '@/components/WebViewFooter';
import { selfUrl } from '@/consts/links'; import { selfUrl } from '@/consts/links';
import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout'; import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout';
import type { SharedRoutesParamList } from '@/navigation/types'; import type { SharedRoutesParamList } from '@/navigation/types';
import {
DISALLOWED_SCHEMES,
isAllowedAboutUrl,
isHostnameMatch,
isTrustedDomain,
isUserInitiatedTopFrameNavigation,
shouldAlwaysOpenExternally,
} from '@/utils/webview';
export interface WebViewScreenParams { export interface WebViewScreenParams {
url: string; url: string;
@@ -41,6 +51,25 @@ type WebViewScreenProps = NativeStackScreenProps<
>; >;
const defaultUrl = selfUrl; 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 }) => { export const WebViewScreen: React.FC<WebViewScreenProps> = ({ route }) => {
const navigation = useNavigation(); const navigation = useNavigation();
@@ -50,24 +79,83 @@ export const WebViewScreen: React.FC<WebViewScreenProps> = ({ route }) => {
const isHttpUrl = useCallback((value?: string) => { const isHttpUrl = useCallback((value?: string) => {
return typeof value === 'string' && /^https?:\/\//i.test(value); return typeof value === 'string' && /^https?:\/\//i.test(value);
}, []); }, []);
const initialUrl = useMemo( const initialUrl = useMemo(() => {
() => (isHttpUrl(url) ? url : defaultUrl), if (isHttpUrl(url) && isTrustedDomain(url)) {
[isHttpUrl, url], return url;
); }
if (isHttpUrl(defaultUrl) && isTrustedDomain(defaultUrl)) {
return defaultUrl;
}
return fallbackUrl;
}, [isHttpUrl, url]);
const webViewRef = useRef<WebViewType>(null); const webViewRef = useRef<WebViewType>(null);
const [canGoBackInWebView, setCanGoBackInWebView] = useState(false); const [canGoBackInWebView, setCanGoBackInWebView] = useState(false);
const [canGoForwardInWebView, setCanGoForwardInWebView] = useState(false); const [canGoForwardInWebView, setCanGoForwardInWebView] = useState(false);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [currentUrl, setCurrentUrl] = useState(initialUrl); const [currentUrl, setCurrentUrl] = useState(initialUrl);
const [pageTitle, setPageTitle] = useState<string | undefined>(title); const [pageTitle, setPageTitle] = useState<string | undefined>(title);
const [isSessionTrusted, setIsSessionTrusted] = useState(
isTrustedDomain(initialUrl),
);
const derivedTitle = pageTitle || title || currentUrl; 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) => { const openUrl = useCallback(async (targetUrl: string) => {
// Allow only safe external schemes // Block disallowed schemes (blacklist approach)
if (!/^(https?|mailto|tel):/i.test(targetUrl)) { // 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; 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 { try {
const supported = await Linking.canOpenURL(targetUrl); const supported = await Linking.canOpenURL(targetUrl);
if (supported) { if (supported) {
@@ -115,16 +203,23 @@ export const WebViewScreen: React.FC<WebViewScreenProps> = ({ route }) => {
const subscription = BackHandler.addEventListener( const subscription = BackHandler.addEventListener(
'hardwareBackPress', 'hardwareBackPress',
() => { () => {
// First try to go back in WebView if possible
if (canGoBackInWebView) { if (canGoBackInWebView) {
webViewRef.current?.goBack(); webViewRef.current?.goBack();
return true; 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 false;
}, },
); );
return () => subscription.remove(); return () => subscription.remove();
}, [canGoBackInWebView]), }, [canGoBackInWebView, navigation]),
); );
return ( return (
@@ -134,6 +229,7 @@ export const WebViewScreen: React.FC<WebViewScreenProps> = ({ route }) => {
alignItems="stretch" alignItems="stretch"
justifyContent="flex-start" justifyContent="flex-start"
padding={0} padding={0}
paddingHorizontal={5}
> >
<WebViewNavBar <WebViewNavBar
title={derivedTitle} title={derivedTitle}
@@ -149,18 +245,158 @@ export const WebViewScreen: React.FC<WebViewScreenProps> = ({ route }) => {
<WebView <WebView
ref={webViewRef} ref={webViewRef}
onShouldStartLoadWithRequest={req => { onShouldStartLoadWithRequest={req => {
// Open non-http(s) externally, block in WebView const isHttps = /^https:\/\//i.test(req.url);
if (!/^https?:\/\//i.test(req.url)) {
openUrl(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 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 }} source={{ uri: initialUrl }}
onNavigationStateChange={(event: WebViewNavigation) => { onNavigationStateChange={(event: WebViewNavigation) => {
setCanGoBackInWebView(event.canGoBack); setCanGoBackInWebView(event.canGoBack);
setCanGoForwardInWebView(event.canGoForward); setCanGoForwardInWebView(event.canGoForward);
setCurrentUrl(prev => (isHttpUrl(event.url) ? event.url : prev)); 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) { if (!title && event.title) {
setPageTitle(event.title); setPageTitle(event.title);
} }
@@ -174,8 +410,8 @@ export const WebViewScreen: React.FC<WebViewScreenProps> = ({ route }) => {
</ExpandableBottomLayout.TopSection> </ExpandableBottomLayout.TopSection>
<ExpandableBottomLayout.BottomSection <ExpandableBottomLayout.BottomSection
backgroundColor={white} backgroundColor={white}
borderTopLeftRadius={30} borderTopLeftRadius={20}
borderTopRightRadius={30} borderTopRightRadius={20}
borderTopWidth={1} borderTopWidth={1}
borderColor={slate200} borderColor={slate200}
style={{ paddingTop: 0 }} style={{ paddingTop: 0 }}
@@ -192,21 +428,3 @@ export const WebViewScreen: React.FC<WebViewScreenProps> = ({ route }) => {
</ExpandableBottomLayout.Layout> </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)',
},
});

View File

@@ -8,3 +8,4 @@
* Use this constant instead of checking __DEV__ directly throughout the codebase. * Use this constant instead of checking __DEV__ directly throughout the codebase.
*/ */
export const IS_DEV_MODE = typeof __DEV__ !== 'undefined' && __DEV__; 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.

View File

@@ -9,6 +9,19 @@
// Crypto utilities // Crypto utilities
export type { ModalCallbacks } from '@/utils/modalCallbackRegistry'; 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 // Format utilities
export { IS_DEV_MODE } from '@/utils/devUtils'; export { IS_DEV_MODE } from '@/utils/devUtils';

193
app/src/utils/webview.ts Normal file
View 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;
}
};

View File

@@ -44,7 +44,7 @@ describe('parseScanResponse', () => {
global.mockPlatformOS = 'ios'; global.mockPlatformOS = 'ios';
}); });
it('parses iOS response', () => { it.skip('parses iOS response', () => {
// Platform.OS is already mocked as 'ios' by default // Platform.OS is already mocked as 'ios' by default
const mrz = const mrz =
'P<UTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<L898902C<3UTO6908061F9406236ZE184226B<<<<<14'; 'P<UTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<L898902C<3UTO6908061F9406236ZE184226B<<<<<14';
@@ -61,8 +61,45 @@ describe('parseScanResponse', () => {
passportPhoto: 'photo', passportPhoto: 'photo',
documentSigningCertificate: JSON.stringify({ PEM: 'CERT' }), 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); 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.mrz).toBe(mrz);
expect(result.documentType).toBe('passport'); expect(result.documentType).toBe('passport');
// 'abcd' in hex: ab = 171, cd = 205 // 'abcd' in hex: ab = 171, cd = 205
@@ -86,8 +123,50 @@ describe('parseScanResponse', () => {
// Android format: '1' and '2' are hex strings, not arrays // Android format: '1' and '2' are hex strings, not arrays
dataGroupHashes: JSON.stringify({ '1': 'abcd', '2': '1234' }), dataGroupHashes: JSON.stringify({ '1': 'abcd', '2': '1234' }),
} as any; } 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); 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.documentType).toBe('passport');
expect(result.mrz).toBe(mrz); expect(result.mrz).toBe(mrz);
// 'abcd' in hex: ab = 171, cd = 205 // 'abcd' in hex: ab = 171, cd = 205

View File

@@ -19,7 +19,9 @@ jest.mock('@/navigation', () => {
// Documents screens // Documents screens
IDPicker: {}, IDPicker: {},
IdDetails: {}, IdDetails: {},
CountryPicker: {}, CountryPicker: {
statusBar: { hidden: true, style: 'dark' },
},
DocumentCamera: {}, DocumentCamera: {},
DocumentCameraTrouble: {}, DocumentCameraTrouble: {},
DocumentDataInfo: {}, DocumentDataInfo: {},

File diff suppressed because it is too large Load Diff

View 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);
});
});
});

View File

@@ -1,10 +1,10 @@
{ {
"ios": { "ios": {
"build": 192, "build": 193,
"lastDeployed": "2025-12-05T00:06:05.459Z" "lastDeployed": "2025-12-06T09:48:56.530Z"
}, },
"android": { "android": {
"build": 123, "build": 124,
"lastDeployed": "2025-11-21T00:06:05.459Z" "lastDeployed": "2025-12-06T09:48:56.530Z"
} }
} }

View File

@@ -113,7 +113,16 @@ template REGISTER_AADHAAR(n, k, maxDataLength){
signal output nullifier <== nullifierHasher.out; 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 // Generate commitment
component packedCommitment = PackBytesAndPoseidon(42 + 62); component packedCommitment = PackBytesAndPoseidon(42 + 62);
@@ -138,7 +147,7 @@ template REGISTER_AADHAAR(n, k, maxDataLength){
component commitmentHasher = Poseidon(5); component commitmentHasher = Poseidon(5);
commitmentHasher.inputs[0] <== secret; commitmentHasher.inputs[0] <== secret;
commitmentHasher.inputs[1] <== qrDataHash; commitmentHasher.inputs[1] <== qrDataHasher.out;
commitmentHasher.inputs[2] <== nullifierHasher.out; commitmentHasher.inputs[2] <== nullifierHasher.out;
commitmentHasher.inputs[3] <== packedCommitment.out; commitmentHasher.inputs[3] <== packedCommitment.out;
commitmentHasher.inputs[4] <== qrDataExtractor.photoHash; commitmentHasher.inputs[4] <== qrDataExtractor.photoHash;

View File

@@ -161,7 +161,7 @@ export const OFAC_TREE_LEVELS = 64;
// we make it global here because passing it to generateCircuitInputsRegister caused trouble // we make it global here because passing it to generateCircuitInputsRegister caused trouble
export const PASSPORT_ATTESTATION_ID = '1'; 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'; export const REDIRECT_URL = 'https://redirect.self.xyz';

View File

@@ -208,8 +208,6 @@ export function prepareAadhaarDiscloseData(
secret, secret,
qrDataHash: formatInput(BigInt(sharedData.qrHash)), qrDataHash: formatInput(BigInt(sharedData.qrHash)),
gender: formatInput(genderAscii), gender: formatInput(genderAscii),
// qrDataHash: BigInt(sharedData.qrHash).toString(),
// gender: genderAscii.toString(),
yob: stringToAsciiArray(sharedData.extractedFields.yob), yob: stringToAsciiArray(sharedData.extractedFields.yob),
mob: stringToAsciiArray(sharedData.extractedFields.mob), mob: stringToAsciiArray(sharedData.extractedFields.mob),
dob: stringToAsciiArray(sharedData.extractedFields.dob), dob: stringToAsciiArray(sharedData.extractedFields.dob),
@@ -551,7 +549,6 @@ export function processQRData(
QRData = newTestData.testQRData; QRData = newTestData.testQRData;
} else { } else {
QRData = testQRData.testQRData; QRData = testQRData.testQRData;
// console.log('testQRData:', testQRData);
} }
return processQRDataSimple(QRData); return processQRDataSimple(QRData);
@@ -576,6 +573,13 @@ export function processQRDataSimple(qrData: string) {
// Extract actual fields from QR data instead of using hardcoded values // Extract actual fields from QR data instead of using hardcoded values
const extractedFields = extractQRDataFields(qrDataBytes); 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 qrHash = packBytesAndPoseidon(Array.from(qrDataPadded));
const photo = extractPhoto(Array.from(qrDataPadded), photoEOI + 1); const photo = extractPhoto(Array.from(qrDataPadded), photoEOI + 1);

View File

@@ -398,7 +398,7 @@ export async function getAadharRegistrationWindow() {
} }
export function returnNewDateString(timestamp?: string): string { 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 // Convert the UTC date to IST by adding 5 hours and 30 minutes
const offsetHours = 5; const offsetHours = 5;

View File

@@ -87,21 +87,19 @@ function compareCertificates(cert1: forge.pki.Certificate, cert2: forge.pki.Cert
} }
function verifyCertificateChain({ leaf, intermediate, root }: PKICertificates) { 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) => { forge.pki.verifyCertificateChain(caStore, [leaf, intermediate, root], (vfd, depth) => {
if (!vfd) { if (vfd !== true) {
throw new Error(`Certificate verification failed at depth ${depth}`); throw new Error(`Certificate verification failed at depth ${depth}`);
} }
return true; return true;
}); });
[leaf, intermediate, root].forEach((cert) => { const now = new Date();
const now = new Date(); if (now < root.validity.notBefore || now > root.validity.notAfter) {
if (now < cert.validity.notBefore || now > cert.validity.notAfter) { throw new Error('Certificate is not within validity period');
throw new Error('Certificate is not within validity period'); }
}
});
} }
/** /**

View File

@@ -8,3 +8,6 @@ CELO_RPC_URL=https://celo.drpc.org
CELO_SEPOLIA_RPC_URL=https://rpc.ankr.com/celo_sepolia CELO_SEPOLIA_RPC_URL=https://rpc.ankr.com/celo_sepolia
ETHERSCAN_API_KEY= ETHERSCAN_API_KEY=
STANDARD_GOVERNANCE_ADDRESS=
CRITICAL_GOVERNANCE_ADDRESS=

214
contracts/UPGRADE_GUIDE.md Normal file
View 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 |

View File

@@ -12,7 +12,6 @@ import {IIdentityRegistryV1} from "./interfaces/IIdentityRegistryV1.sol";
import {IRegisterCircuitVerifier} from "./interfaces/IRegisterCircuitVerifier.sol"; import {IRegisterCircuitVerifier} from "./interfaces/IRegisterCircuitVerifier.sol";
import {IVcAndDiscloseCircuitVerifier} from "./interfaces/IVcAndDiscloseCircuitVerifier.sol"; import {IVcAndDiscloseCircuitVerifier} from "./interfaces/IVcAndDiscloseCircuitVerifier.sol";
import {IDscCircuitVerifier} from "./interfaces/IDscCircuitVerifier.sol"; import {IDscCircuitVerifier} from "./interfaces/IDscCircuitVerifier.sol";
import {ImplRoot} from "./upgradeable/ImplRoot.sol";
/** /**
* @notice ⚠️ CRITICAL STORAGE LAYOUT WARNING ⚠️ * @notice ⚠️ CRITICAL STORAGE LAYOUT WARNING ⚠️
@@ -43,9 +42,12 @@ import {ImplRoot} from "./upgradeable/ImplRoot.sol";
/** /**
* @title IdentityVerificationHubStorageV1 * @title IdentityVerificationHubStorageV1
* @notice Storage contract for IdentityVerificationHubImplV1. * @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 // Storage Variables
// ==================================================== // ====================================================
@@ -61,6 +63,14 @@ abstract contract IdentityVerificationHubStorageV1 is ImplRoot {
/// @notice Mapping from signature type to DSC circuit verifier addresses.. /// @notice Mapping from signature type to DSC circuit verifier addresses..
mapping(uint256 => address) internal _sigTypeToDscCircuitVerifiers; 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, uint256[] memory dscCircuitVerifierIds,
address[] memory dscCircuitVerifierAddresses address[] memory dscCircuitVerifierAddresses
) external initializer { ) external initializer {
__ImplRoot_init(); __Ownable_init(msg.sender);
_registry = registryAddress; _registry = registryAddress;
_vcAndDiscloseCircuitVerifier = vcAndDiscloseCircuitVerifierAddress; _vcAndDiscloseCircuitVerifier = vcAndDiscloseCircuitVerifierAddress;
if (registerCircuitVerifierIds.length != registerCircuitVerifierAddresses.length) { if (registerCircuitVerifierIds.length != registerCircuitVerifierAddresses.length) {

View File

@@ -19,6 +19,14 @@ import {IDscCircuitVerifier} from "./interfaces/IDscCircuitVerifier.sol";
import {CircuitConstantsV2} from "./constants/CircuitConstantsV2.sol"; import {CircuitConstantsV2} from "./constants/CircuitConstantsV2.sol";
import {Formatter} from "./libraries/Formatter.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 { contract IdentityVerificationHubImplV2 is ImplRoot {
/// @custom:storage-location erc7201:self.storage.IdentityVerificationHub /// @custom:storage-location erc7201:self.storage.IdentityVerificationHub
struct IdentityVerificationHubStorage { struct IdentityVerificationHubStorage {
@@ -45,7 +53,7 @@ contract IdentityVerificationHubImplV2 is ImplRoot {
0xf9b5980dcec1a8b0609576a1f453bb2cad4732a0ea02bb89154d44b14a306c00; 0xf9b5980dcec1a8b0609576a1f453bb2cad4732a0ea02bb89154d44b14a306c00;
/// @notice The AADHAAR registration window around the current block timestamp. /// @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. * @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. * @notice Constructor that disables initializers for the implementation contract.
* @dev This prevents the implementation contract from being initialized directly. * @dev This prevents the implementation contract from being initialized directly.
* The actual initialization should only happen through the proxy. * The actual initialization should only happen through the proxy.
* @custom:oz-upgrades-unsafe-allow constructor
*/ */
constructor() { constructor() {
_disableInitializers(); _disableInitializers();
@@ -240,9 +249,25 @@ contract IdentityVerificationHubImplV2 is ImplRoot {
IdentityVerificationHubStorage storage $ = _getIdentityVerificationHubStorage(); IdentityVerificationHubStorage storage $ = _getIdentityVerificationHubStorage();
$._circuitVersion = 2; $._circuitVersion = 2;
// Initialize Aadhaar registration window
AADHAAR_REGISTRATION_WINDOW = 20;
emit HubInitializedV2(); 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 // External Functions
// ==================================================== // ====================================================
@@ -329,7 +354,7 @@ contract IdentityVerificationHubImplV2 is ImplRoot {
* @notice Updates the AADHAAR registration window. * @notice Updates the AADHAAR registration window.
* @param window The new 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; AADHAAR_REGISTRATION_WINDOW = window;
} }
@@ -372,7 +397,10 @@ contract IdentityVerificationHubImplV2 is ImplRoot {
* @notice Updates the registry address. * @notice Updates the registry address.
* @param registryAddress The new 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(); IdentityVerificationHubStorage storage $ = _getIdentityVerificationHubStorage();
$._registries[attestationId] = registryAddress; $._registries[attestationId] = registryAddress;
emit RegistryUpdated(attestationId, registryAddress); emit RegistryUpdated(attestationId, registryAddress);
@@ -385,7 +413,7 @@ contract IdentityVerificationHubImplV2 is ImplRoot {
function updateVcAndDiscloseCircuit( function updateVcAndDiscloseCircuit(
bytes32 attestationId, bytes32 attestationId,
address vcAndDiscloseCircuitVerifierAddress address vcAndDiscloseCircuitVerifierAddress
) external virtual onlyProxy onlyOwner { ) external virtual onlyProxy onlyRole(SECURITY_ROLE) {
IdentityVerificationHubStorage storage $ = _getIdentityVerificationHubStorage(); IdentityVerificationHubStorage storage $ = _getIdentityVerificationHubStorage();
$._discloseVerifiers[attestationId] = vcAndDiscloseCircuitVerifierAddress; $._discloseVerifiers[attestationId] = vcAndDiscloseCircuitVerifierAddress;
emit VcAndDiscloseCircuitUpdated(attestationId, vcAndDiscloseCircuitVerifierAddress); emit VcAndDiscloseCircuitUpdated(attestationId, vcAndDiscloseCircuitVerifierAddress);
@@ -401,7 +429,7 @@ contract IdentityVerificationHubImplV2 is ImplRoot {
bytes32 attestationId, bytes32 attestationId,
uint256 typeId, uint256 typeId,
address verifierAddress address verifierAddress
) external virtual onlyProxy onlyOwner { ) external virtual onlyProxy onlyRole(SECURITY_ROLE) {
IdentityVerificationHubStorage storage $ = _getIdentityVerificationHubStorage(); IdentityVerificationHubStorage storage $ = _getIdentityVerificationHubStorage();
$._registerCircuitVerifiers[attestationId][typeId] = verifierAddress; $._registerCircuitVerifiers[attestationId][typeId] = verifierAddress;
emit RegisterCircuitVerifierUpdated(typeId, verifierAddress); emit RegisterCircuitVerifierUpdated(typeId, verifierAddress);
@@ -417,7 +445,7 @@ contract IdentityVerificationHubImplV2 is ImplRoot {
bytes32 attestationId, bytes32 attestationId,
uint256 typeId, uint256 typeId,
address verifierAddress address verifierAddress
) external virtual onlyProxy onlyOwner { ) external virtual onlyProxy onlyRole(SECURITY_ROLE) {
IdentityVerificationHubStorage storage $ = _getIdentityVerificationHubStorage(); IdentityVerificationHubStorage storage $ = _getIdentityVerificationHubStorage();
$._dscCircuitVerifiers[attestationId][typeId] = verifierAddress; $._dscCircuitVerifiers[attestationId][typeId] = verifierAddress;
emit DscCircuitVerifierUpdated(typeId, verifierAddress); emit DscCircuitVerifierUpdated(typeId, verifierAddress);
@@ -433,7 +461,7 @@ contract IdentityVerificationHubImplV2 is ImplRoot {
bytes32[] calldata attestationIds, bytes32[] calldata attestationIds,
uint256[] calldata typeIds, uint256[] calldata typeIds,
address[] calldata verifierAddresses address[] calldata verifierAddresses
) external virtual onlyProxy onlyOwner { ) external virtual onlyProxy onlyRole(SECURITY_ROLE) {
if (attestationIds.length != typeIds.length || attestationIds.length != verifierAddresses.length) { if (attestationIds.length != typeIds.length || attestationIds.length != verifierAddresses.length) {
revert LengthMismatch(); revert LengthMismatch();
} }
@@ -454,7 +482,7 @@ contract IdentityVerificationHubImplV2 is ImplRoot {
bytes32[] calldata attestationIds, bytes32[] calldata attestationIds,
uint256[] calldata typeIds, uint256[] calldata typeIds,
address[] calldata verifierAddresses address[] calldata verifierAddresses
) external virtual onlyProxy onlyOwner { ) external virtual onlyProxy onlyRole(SECURITY_ROLE) {
if (attestationIds.length != typeIds.length || attestationIds.length != verifierAddresses.length) { if (attestationIds.length != typeIds.length || attestationIds.length != verifierAddresses.length) {
revert LengthMismatch(); revert LengthMismatch();
} }
@@ -677,15 +705,11 @@ contract IdentityVerificationHubImplV2 is ImplRoot {
_performUserIdentifierCheck(userContextData, vcAndDiscloseProof, header.attestationId, indices); _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); _performRootCheck(header.attestationId, vcAndDiscloseProof, indices);
_performOfacCheck(header.attestationId, vcAndDiscloseProof, indices); _performOfacCheck(header.attestationId, vcAndDiscloseProof, indices);
if (header.attestationId == AttestationId.AADHAAR) { _performCurrentDateCheck(header.attestationId, vcAndDiscloseProof, indices);
_performNumericCurrentDateCheck(vcAndDiscloseProof, indices);
} else {
_performCurrentDateCheck(vcAndDiscloseProof, indices);
}
} }
// Scope 3: Groth16 proof verification // 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( function _performCurrentDateCheck(
bytes32 attestationId,
GenericProofStruct memory vcAndDiscloseProof, GenericProofStruct memory vcAndDiscloseProof,
CircuitConstantsV2.DiscloseIndices memory indices CircuitConstantsV2.DiscloseIndices memory indices
) internal view { ) internal view {
uint256[6] memory dateNum; uint256 currentTimestamp;
for (uint256 i = 0; i < 6; i++) { uint256 startIndex = indices.currentDateIndex;
dateNum[i] = vcAndDiscloseProof.pubSignals[indices.currentDateIndex + i];
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); _validateDateInRange(currentTimestamp);
uint256 startOfDay = _getStartOfDayTimestamp();
uint256 endOfDay = startOfDay + 1 days - 1;
if (currentTimestamp < startOfDay - 1 days + 1 || currentTimestamp > endOfDay + 1 days) {
revert CurrentDateNotInValidRange();
}
} }
function _performNumericCurrentDateCheck( /**
GenericProofStruct memory vcAndDiscloseProof, * @notice Validates that a timestamp is within the acceptable range
CircuitConstantsV2.DiscloseIndices memory indices * @param currentTimestamp The timestamp to validate
) internal view { */
// date is going to be 2025, 12, 13 function _validateDateInRange(uint256 currentTimestamp) internal view {
uint256[3] memory dateNum; // Calculate the timestamp for the start of current date by subtracting the remainder of block.timestamp modulo 1 day
dateNum[0] = vcAndDiscloseProof.pubSignals[indices.currentDateIndex]; uint256 startOfDay = block.timestamp - (block.timestamp % 1 days);
dateNum[1] = vcAndDiscloseProof.pubSignals[indices.currentDateIndex + 1];
dateNum[2] = vcAndDiscloseProof.pubSignals[indices.currentDateIndex + 2];
uint256 currentTimestamp = Formatter.proofDateToUnixTimestampNumeric(dateNum); // Check if timestamp is within range
uint256 startOfDay = _getStartOfDayTimestamp(); if (currentTimestamp < startOfDay - 1 days + 1 || currentTimestamp > startOfDay + 1 days - 1) {
uint256 endOfDay = startOfDay + 1 days - 1;
if (currentTimestamp < startOfDay - 1 days + 1 || currentTimestamp > endOfDay + 1 days) {
revert CurrentDateNotInValidRange(); revert CurrentDateNotInValidRange();
} }
} }

View File

@@ -49,3 +49,21 @@ interface IVcAndDiscloseAadhaarCircuitVerifier {
uint256[19] calldata pubSignals uint256[19] calldata pubSignals
) external view returns (bool); ) 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);
}

View File

@@ -70,6 +70,8 @@ abstract contract IdentityRegistryAadhaarStorageV1 is ImplRoot {
* @title IdentityRegistryAadhaarImplV1 * @title IdentityRegistryAadhaarImplV1
* @notice Provides functions to register and manage identity commitments using a Merkle tree structure. * @notice Provides functions to register and manage identity commitments using a Merkle tree structure.
* @dev Inherits from IdentityRegistryAadhaarStorageV1 and implements IIdentityRegistryAadhaarV1. * @dev Inherits from IdentityRegistryAadhaarStorageV1 and implements IIdentityRegistryAadhaarV1.
*
* @custom:version 1.2.0
*/ */
contract IdentityRegistryAadhaarImplV1 is IdentityRegistryAadhaarStorageV1, IIdentityRegistryAadhaarV1 { contract IdentityRegistryAadhaarImplV1 is IdentityRegistryAadhaarStorageV1, IIdentityRegistryAadhaarV1 {
using InternalLeanIMT for LeanIMTData; using InternalLeanIMT for LeanIMTData;
@@ -151,6 +153,7 @@ contract IdentityRegistryAadhaarImplV1 is IdentityRegistryAadhaarStorageV1, IIde
// ==================================================== // ====================================================
/// @notice Constructor for the IdentityRegistryAadhaarImplV1 contract. /// @notice Constructor for the IdentityRegistryAadhaarImplV1 contract.
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() { constructor() {
_disableInitializers(); _disableInitializers();
} }
@@ -168,6 +171,19 @@ contract IdentityRegistryAadhaarImplV1 is IdentityRegistryAadhaarStorageV1, IIde
emit RegistryInitialized(_hub); 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 // External Functions - View & Checks
// ==================================================== // ====================================================
@@ -280,7 +296,7 @@ contract IdentityRegistryAadhaarImplV1 is IdentityRegistryAadhaarStorageV1, IIde
/// @notice Updates the hub address. /// @notice Updates the hub address.
/// @dev Callable only via a proxy and restricted to the contract owner. /// @dev Callable only via a proxy and restricted to the contract owner.
/// @param newHubAddress The new address of the hub. /// @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(); if (newHubAddress == address(0)) revert HUB_ADDRESS_ZERO();
_hub = newHubAddress; _hub = newHubAddress;
emit HubUpdated(newHubAddress); emit HubUpdated(newHubAddress);
@@ -289,7 +305,7 @@ contract IdentityRegistryAadhaarImplV1 is IdentityRegistryAadhaarStorageV1, IIde
/// @notice Updates the name and date of birth OFAC root. /// @notice Updates the name and date of birth OFAC root.
/// @dev Callable only via a proxy and restricted to the contract owner. /// @dev Callable only via a proxy and restricted to the contract owner.
/// @param newNameAndDobOfacRoot The new name and date of birth OFAC root value. /// @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; _nameAndDobOfacRoot = newNameAndDobOfacRoot;
emit NameAndDobOfacRootUpdated(newNameAndDobOfacRoot); emit NameAndDobOfacRootUpdated(newNameAndDobOfacRoot);
} }
@@ -297,7 +313,7 @@ contract IdentityRegistryAadhaarImplV1 is IdentityRegistryAadhaarStorageV1, IIde
/// @notice Updates the name and year of birth OFAC root. /// @notice Updates the name and year of birth OFAC root.
/// @dev Callable only via a proxy and restricted to the contract owner. /// @dev Callable only via a proxy and restricted to the contract owner.
/// @param newNameAndYobOfacRoot The new name and year of birth OFAC root value. /// @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; _nameAndYobOfacRoot = newNameAndYobOfacRoot;
emit NameAndYobOfacRootUpdated(newNameAndYobOfacRoot); emit NameAndYobOfacRootUpdated(newNameAndYobOfacRoot);
} }
@@ -305,7 +321,7 @@ contract IdentityRegistryAadhaarImplV1 is IdentityRegistryAadhaarStorageV1, IIde
/// @notice Registers a new UIDAI pubkey commitment. /// @notice Registers a new UIDAI pubkey commitment.
/// @dev Callable only via a proxy and restricted to the contract owner. /// @dev Callable only via a proxy and restricted to the contract owner.
/// @param commitment The UIDAI pubkey commitment to register. /// @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; _uidaiPubkeyCommitments[commitment] = true;
emit UidaiPubkeyCommitmentRegistered(commitment, block.timestamp); emit UidaiPubkeyCommitmentRegistered(commitment, block.timestamp);
} }
@@ -313,7 +329,7 @@ contract IdentityRegistryAadhaarImplV1 is IdentityRegistryAadhaarStorageV1, IIde
/// @notice Removes a UIDAI pubkey commitment. /// @notice Removes a UIDAI pubkey commitment.
/// @dev Callable only via a proxy and restricted to the contract owner. /// @dev Callable only via a proxy and restricted to the contract owner.
/// @param commitment The UIDAI pubkey commitment to remove. /// @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]; delete _uidaiPubkeyCommitments[commitment];
emit UidaiPubkeyCommitmentRemoved(commitment, block.timestamp); emit UidaiPubkeyCommitmentRemoved(commitment, block.timestamp);
} }
@@ -321,7 +337,7 @@ contract IdentityRegistryAadhaarImplV1 is IdentityRegistryAadhaarStorageV1, IIde
/// @notice Updates a UIDAI pubkey commitment. /// @notice Updates a UIDAI pubkey commitment.
/// @dev Callable only via a proxy and restricted to the contract owner. /// @dev Callable only via a proxy and restricted to the contract owner.
/// @param commitment The UIDAI pubkey commitment to update. /// @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; _uidaiPubkeyCommitments[commitment] = true;
emit UidaiPubkeyCommitmentUpdated(commitment, block.timestamp); emit UidaiPubkeyCommitmentUpdated(commitment, block.timestamp);
} }
@@ -335,7 +351,7 @@ contract IdentityRegistryAadhaarImplV1 is IdentityRegistryAadhaarStorageV1, IIde
bytes32 attestationId, bytes32 attestationId,
uint256 nullifier, uint256 nullifier,
uint256 commitment uint256 commitment
) external onlyProxy onlyOwner { ) external onlyProxy onlyRole(SECURITY_ROLE) {
_nullifiers[nullifier] = true; _nullifiers[nullifier] = true;
uint256 imt_root = _identityCommitmentIMT._insert(commitment); uint256 imt_root = _identityCommitmentIMT._insert(commitment);
_rootTimestamps[imt_root] = block.timestamp; _rootTimestamps[imt_root] = block.timestamp;
@@ -352,7 +368,7 @@ contract IdentityRegistryAadhaarImplV1 is IdentityRegistryAadhaarStorageV1, IIde
uint256 oldLeaf, uint256 oldLeaf,
uint256 newLeaf, uint256 newLeaf,
uint256[] calldata siblingNodes uint256[] calldata siblingNodes
) external onlyProxy onlyOwner { ) external onlyProxy onlyRole(SECURITY_ROLE) {
uint256 imt_root = _identityCommitmentIMT._update(oldLeaf, newLeaf, siblingNodes); uint256 imt_root = _identityCommitmentIMT._update(oldLeaf, newLeaf, siblingNodes);
_rootTimestamps[imt_root] = block.timestamp; _rootTimestamps[imt_root] = block.timestamp;
emit DevCommitmentUpdated(oldLeaf, newLeaf, 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. /// @dev Caller must be the owner. Provides sibling nodes for proof of position.
/// @param oldLeaf The identity commitment to remove. /// @param oldLeaf The identity commitment to remove.
/// @param siblingNodes An array of sibling nodes for Merkle proof generation. /// @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); uint256 imt_root = _identityCommitmentIMT._remove(oldLeaf, siblingNodes);
_rootTimestamps[imt_root] = block.timestamp; _rootTimestamps[imt_root] = block.timestamp;
emit DevCommitmentRemoved(oldLeaf, imt_root, block.timestamp); emit DevCommitmentRemoved(oldLeaf, imt_root, block.timestamp);

View File

@@ -77,6 +77,8 @@ abstract contract IdentityRegistryIdCardStorageV1 is ImplRoot {
* @title IdentityRegistryImplV1 * @title IdentityRegistryImplV1
* @notice Provides functions to register and manage identity commitments using a Merkle tree structure. * @notice Provides functions to register and manage identity commitments using a Merkle tree structure.
* @dev Inherits from IdentityRegistryStorageV1 and implements IIdentityRegistryV1. * @dev Inherits from IdentityRegistryStorageV1 and implements IIdentityRegistryV1.
*
* @custom:version 1.2.0
*/ */
contract IdentityRegistryIdCardImplV1 is IdentityRegistryIdCardStorageV1, IIdentityRegistryIdCardV1 { contract IdentityRegistryIdCardImplV1 is IdentityRegistryIdCardStorageV1, IIdentityRegistryIdCardV1 {
using InternalLeanIMT for LeanIMTData; using InternalLeanIMT for LeanIMTData;
@@ -162,6 +164,7 @@ contract IdentityRegistryIdCardImplV1 is IdentityRegistryIdCardStorageV1, IIdent
/** /**
* @notice Constructor that disables initializers. * @notice Constructor that disables initializers.
* @dev Prevents direct initialization of the implementation contract. * @dev Prevents direct initialization of the implementation contract.
* @custom:oz-upgrades-unsafe-allow constructor
*/ */
constructor() { constructor() {
_disableInitializers(); _disableInitializers();
@@ -181,6 +184,19 @@ contract IdentityRegistryIdCardImplV1 is IdentityRegistryIdCardStorageV1, IIdent
emit RegistryInitialized(_hub); 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 // 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. * @dev Callable only via a proxy and restricted to the contract owner.
* @param newHubAddress The new address of the hub. * @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; _hub = newHubAddress;
emit HubUpdated(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. * @dev Callable only via a proxy and restricted to the contract owner.
* @param newNameAndDobOfacRoot The new name and date of birth OFAC root value. * @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; _nameAndDobOfacRoot = newNameAndDobOfacRoot;
emit NameAndDobOfacRootUpdated(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. * @dev Callable only via a proxy and restricted to the contract owner.
* @param newNameAndYobOfacRoot The new name and year of birth OFAC root value. * @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; _nameAndYobOfacRoot = newNameAndYobOfacRoot;
emit NameAndYobOfacRootUpdated(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. * @dev Callable only via a proxy and restricted to the contract owner.
* @param newCscaRoot The new CSCA root value. * @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; _cscaRoot = newCscaRoot;
emit CscaRootUpdated(newCscaRoot); emit CscaRootUpdated(newCscaRoot);
} }
@@ -426,7 +442,7 @@ contract IdentityRegistryIdCardImplV1 is IdentityRegistryIdCardStorageV1, IIdent
bytes32 attestationId, bytes32 attestationId,
uint256 nullifier, uint256 nullifier,
uint256 commitment uint256 commitment
) external onlyProxy onlyOwner { ) external onlyProxy onlyRole(SECURITY_ROLE) {
_nullifiers[attestationId][nullifier] = true; _nullifiers[attestationId][nullifier] = true;
uint256 imt_root = _addCommitment(_identityCommitmentIMT, commitment); uint256 imt_root = _addCommitment(_identityCommitmentIMT, commitment);
_rootTimestamps[imt_root] = block.timestamp; _rootTimestamps[imt_root] = block.timestamp;
@@ -445,7 +461,7 @@ contract IdentityRegistryIdCardImplV1 is IdentityRegistryIdCardStorageV1, IIdent
uint256 oldLeaf, uint256 oldLeaf,
uint256 newLeaf, uint256 newLeaf,
uint256[] calldata siblingNodes uint256[] calldata siblingNodes
) external onlyProxy onlyOwner { ) external onlyProxy onlyRole(SECURITY_ROLE) {
uint256 imt_root = _updateCommitment(_identityCommitmentIMT, oldLeaf, newLeaf, siblingNodes); uint256 imt_root = _updateCommitment(_identityCommitmentIMT, oldLeaf, newLeaf, siblingNodes);
_rootTimestamps[imt_root] = block.timestamp; _rootTimestamps[imt_root] = block.timestamp;
emit DevCommitmentUpdated(oldLeaf, newLeaf, 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 oldLeaf The identity commitment to remove.
* @param siblingNodes An array of sibling nodes for Merkle proof generation. * @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); uint256 imt_root = _removeCommitment(_identityCommitmentIMT, oldLeaf, siblingNodes);
_rootTimestamps[imt_root] = block.timestamp; _rootTimestamps[imt_root] = block.timestamp;
emit DevCommitmentRemoved(oldLeaf, 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. * @dev Callable only by the owner for testing or administration.
* @param dscCommitment The DSC key commitment to add. * @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; _isRegisteredDscKeyCommitment[dscCommitment] = true;
uint256 imt_root = _addCommitment(_dscKeyCommitmentIMT, dscCommitment); uint256 imt_root = _addCommitment(_dscKeyCommitmentIMT, dscCommitment);
uint256 index = _dscKeyCommitmentIMT._indexOf(dscCommitment); uint256 index = _dscKeyCommitmentIMT._indexOf(dscCommitment);
@@ -486,7 +505,7 @@ contract IdentityRegistryIdCardImplV1 is IdentityRegistryIdCardStorageV1, IIdent
uint256 oldLeaf, uint256 oldLeaf,
uint256 newLeaf, uint256 newLeaf,
uint256[] calldata siblingNodes uint256[] calldata siblingNodes
) external onlyProxy onlyOwner { ) external onlyProxy onlyRole(SECURITY_ROLE) {
uint256 imt_root = _updateCommitment(_dscKeyCommitmentIMT, oldLeaf, newLeaf, siblingNodes); uint256 imt_root = _updateCommitment(_dscKeyCommitmentIMT, oldLeaf, newLeaf, siblingNodes);
emit DevDscKeyCommitmentUpdated(oldLeaf, newLeaf, imt_root); emit DevDscKeyCommitmentUpdated(oldLeaf, newLeaf, imt_root);
} }
@@ -497,7 +516,10 @@ contract IdentityRegistryIdCardImplV1 is IdentityRegistryIdCardStorageV1, IIdent
* @param oldLeaf The DSC key commitment to remove. * @param oldLeaf The DSC key commitment to remove.
* @param siblingNodes An array of sibling nodes for Merkle proof generation. * @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); uint256 imt_root = _removeCommitment(_dscKeyCommitmentIMT, oldLeaf, siblingNodes);
emit DevDscKeyCommitmentRemoved(oldLeaf, imt_root); emit DevDscKeyCommitmentRemoved(oldLeaf, imt_root);
} }
@@ -513,7 +535,7 @@ contract IdentityRegistryIdCardImplV1 is IdentityRegistryIdCardStorageV1, IIdent
bytes32 attestationId, bytes32 attestationId,
uint256 nullifier, uint256 nullifier,
bool state bool state
) external onlyProxy onlyOwner { ) external onlyProxy onlyRole(SECURITY_ROLE) {
_nullifiers[attestationId][nullifier] = state; _nullifiers[attestationId][nullifier] = state;
emit DevNullifierStateChanged(attestationId, nullifier, state); emit DevNullifierStateChanged(attestationId, nullifier, state);
} }
@@ -524,7 +546,10 @@ contract IdentityRegistryIdCardImplV1 is IdentityRegistryIdCardStorageV1, IIdent
* @param dscCommitment The DSC key commitment. * @param dscCommitment The DSC key commitment.
* @param state The new state of the DSC key commitment (true for registered, false for not registered). * @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; _isRegisteredDscKeyCommitment[dscCommitment] = state;
emit DevDscKeyCommitmentStateChanged(dscCommitment, state); emit DevDscKeyCommitmentStateChanged(dscCommitment, state);
} }

View File

@@ -82,6 +82,8 @@ abstract contract IdentityRegistryStorageV1 is ImplRoot {
* @title IdentityRegistryImplV1 * @title IdentityRegistryImplV1
* @notice Provides functions to register and manage identity commitments using a Merkle tree structure. * @notice Provides functions to register and manage identity commitments using a Merkle tree structure.
* @dev Inherits from IdentityRegistryStorageV1 and implements IIdentityRegistryV1. * @dev Inherits from IdentityRegistryStorageV1 and implements IIdentityRegistryV1.
*
* @custom:version 1.2.0
*/ */
contract IdentityRegistryImplV1 is IdentityRegistryStorageV1, IIdentityRegistryV1 { contract IdentityRegistryImplV1 is IdentityRegistryStorageV1, IIdentityRegistryV1 {
using InternalLeanIMT for LeanIMTData; using InternalLeanIMT for LeanIMTData;
@@ -169,6 +171,7 @@ contract IdentityRegistryImplV1 is IdentityRegistryStorageV1, IIdentityRegistryV
/** /**
* @notice Constructor that disables initializers. * @notice Constructor that disables initializers.
* @dev Prevents direct initialization of the implementation contract. * @dev Prevents direct initialization of the implementation contract.
* @custom:oz-upgrades-unsafe-allow constructor
*/ */
constructor() { constructor() {
_disableInitializers(); _disableInitializers();
@@ -188,6 +191,19 @@ contract IdentityRegistryImplV1 is IdentityRegistryStorageV1, IIdentityRegistryV
emit RegistryInitialized(hubAddress); 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 // 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. * @dev Callable only via a proxy and restricted to the contract owner.
* @param newHubAddress The new address of the hub. * @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; _hub = newHubAddress;
emit HubUpdated(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. * @dev Callable only via a proxy and restricted to the contract owner.
* @param newPassportNoOfacRoot The new passport number OFAC root value. * @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; _passportNoOfacRoot = newPassportNoOfacRoot;
emit PassportNoOfacRootUpdated(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. * @dev Callable only via a proxy and restricted to the contract owner.
* @param newNameAndDobOfacRoot The new name and date of birth OFAC root value. * @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; _nameAndDobOfacRoot = newNameAndDobOfacRoot;
emit NameAndDobOfacRootUpdated(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. * @dev Callable only via a proxy and restricted to the contract owner.
* @param newNameAndYobOfacRoot The new name and year of birth OFAC root value. * @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; _nameAndYobOfacRoot = newNameAndYobOfacRoot;
emit NameAndYobOfacRootUpdated(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. * @dev Callable only via a proxy and restricted to the contract owner.
* @param newCscaRoot The new CSCA root value. * @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; _cscaRoot = newCscaRoot;
emit CscaRootUpdated(newCscaRoot); emit CscaRootUpdated(newCscaRoot);
} }
@@ -459,7 +475,7 @@ contract IdentityRegistryImplV1 is IdentityRegistryStorageV1, IIdentityRegistryV
bytes32 attestationId, bytes32 attestationId,
uint256 nullifier, uint256 nullifier,
uint256 commitment uint256 commitment
) external onlyProxy onlyOwner { ) external onlyProxy onlyRole(SECURITY_ROLE) {
_nullifiers[attestationId][nullifier] = true; _nullifiers[attestationId][nullifier] = true;
uint256 imt_root = _addCommitment(_identityCommitmentIMT, commitment); uint256 imt_root = _addCommitment(_identityCommitmentIMT, commitment);
_rootTimestamps[imt_root] = block.timestamp; _rootTimestamps[imt_root] = block.timestamp;
@@ -478,7 +494,7 @@ contract IdentityRegistryImplV1 is IdentityRegistryStorageV1, IIdentityRegistryV
uint256 oldLeaf, uint256 oldLeaf,
uint256 newLeaf, uint256 newLeaf,
uint256[] calldata siblingNodes uint256[] calldata siblingNodes
) external onlyProxy onlyOwner { ) external onlyProxy onlyRole(SECURITY_ROLE) {
uint256 imt_root = _updateCommitment(_identityCommitmentIMT, oldLeaf, newLeaf, siblingNodes); uint256 imt_root = _updateCommitment(_identityCommitmentIMT, oldLeaf, newLeaf, siblingNodes);
_rootTimestamps[imt_root] = block.timestamp; _rootTimestamps[imt_root] = block.timestamp;
emit DevCommitmentUpdated(oldLeaf, newLeaf, 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 oldLeaf The identity commitment to remove.
* @param siblingNodes An array of sibling nodes for Merkle proof generation. * @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); uint256 imt_root = _removeCommitment(_identityCommitmentIMT, oldLeaf, siblingNodes);
_rootTimestamps[imt_root] = block.timestamp; _rootTimestamps[imt_root] = block.timestamp;
emit DevCommitmentRemoved(oldLeaf, 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. * @dev Callable only by the owner for testing or administration.
* @param dscCommitment The DSC key commitment to add. * @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; _isRegisteredDscKeyCommitment[dscCommitment] = true;
uint256 imt_root = _addCommitment(_dscKeyCommitmentIMT, dscCommitment); uint256 imt_root = _addCommitment(_dscKeyCommitmentIMT, dscCommitment);
uint256 index = _dscKeyCommitmentIMT._indexOf(dscCommitment); uint256 index = _dscKeyCommitmentIMT._indexOf(dscCommitment);
@@ -519,7 +538,7 @@ contract IdentityRegistryImplV1 is IdentityRegistryStorageV1, IIdentityRegistryV
uint256 oldLeaf, uint256 oldLeaf,
uint256 newLeaf, uint256 newLeaf,
uint256[] calldata siblingNodes uint256[] calldata siblingNodes
) external onlyProxy onlyOwner { ) external onlyProxy onlyRole(SECURITY_ROLE) {
uint256 imt_root = _updateCommitment(_dscKeyCommitmentIMT, oldLeaf, newLeaf, siblingNodes); uint256 imt_root = _updateCommitment(_dscKeyCommitmentIMT, oldLeaf, newLeaf, siblingNodes);
emit DevDscKeyCommitmentUpdated(oldLeaf, newLeaf, imt_root); emit DevDscKeyCommitmentUpdated(oldLeaf, newLeaf, imt_root);
} }
@@ -530,7 +549,10 @@ contract IdentityRegistryImplV1 is IdentityRegistryStorageV1, IIdentityRegistryV
* @param oldLeaf The DSC key commitment to remove. * @param oldLeaf The DSC key commitment to remove.
* @param siblingNodes An array of sibling nodes for Merkle proof generation. * @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); uint256 imt_root = _removeCommitment(_dscKeyCommitmentIMT, oldLeaf, siblingNodes);
emit DevDscKeyCommitmentRemoved(oldLeaf, imt_root); emit DevDscKeyCommitmentRemoved(oldLeaf, imt_root);
} }
@@ -546,7 +568,7 @@ contract IdentityRegistryImplV1 is IdentityRegistryStorageV1, IIdentityRegistryV
bytes32 attestationId, bytes32 attestationId,
uint256 nullifier, uint256 nullifier,
bool state bool state
) external onlyProxy onlyOwner { ) external onlyProxy onlyRole(SECURITY_ROLE) {
_nullifiers[attestationId][nullifier] = state; _nullifiers[attestationId][nullifier] = state;
emit DevNullifierStateChanged(attestationId, nullifier, state); emit DevNullifierStateChanged(attestationId, nullifier, state);
} }
@@ -557,7 +579,10 @@ contract IdentityRegistryImplV1 is IdentityRegistryStorageV1, IIdentityRegistryV
* @param dscCommitment The DSC key commitment. * @param dscCommitment The DSC key commitment.
* @param state The new state of the DSC key commitment (true for registered, false for not registered). * @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; _isRegisteredDscKeyCommitment[dscCommitment] = state;
emit DevDscKeyCommitmentStateChanged(dscCommitment, state); emit DevDscKeyCommitmentStateChanged(dscCommitment, state);
} }

View File

@@ -3,22 +3,36 @@ pragma solidity 0.8.28;
import {IIdentityVerificationHubV1} from "../interfaces/IIdentityVerificationHubV1.sol"; import {IIdentityVerificationHubV1} from "../interfaces/IIdentityVerificationHubV1.sol";
import {IIdentityRegistryV1} from "../interfaces/IIdentityRegistryV1.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"; import {CircuitConstants} from "../constants/CircuitConstants.sol";
/// @title VerifyAll /// @title VerifyAll
/// @notice A contract for verifying identity proofs and revealing selected data /// @notice A contract for verifying identity proofs and revealing selected data
/// @dev This contract interacts with IdentityVerificationHub and IdentityRegistry /// @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; IIdentityVerificationHubV1 public hub;
IIdentityRegistryV1 public registry; IIdentityRegistryV1 public registry;
/// @notice Initializes the contract with hub and registry addresses /// @notice Initializes the contract with hub and registry addresses
/// @param hubAddress The address of the IdentityVerificationHub contract /// @param hubAddress The address of the IdentityVerificationHub contract
/// @param registryAddress The address of the IdentityRegistry 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); hub = IIdentityVerificationHubV1(hubAddress);
registry = IIdentityRegistryV1(registryAddress); 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 /// @notice Verifies identity proof and reveals selected data
@@ -107,15 +121,15 @@ contract VerifyAll is Ownable {
/// @notice Updates the hub contract address /// @notice Updates the hub contract address
/// @param hubAddress The new hub contract address /// @param hubAddress The new hub contract address
/// @dev Only callable by the contract owner /// @dev Only callable by accounts with SECURITY_ROLE
function setHub(address hubAddress) external onlyOwner { function setHub(address hubAddress) external onlyRole(SECURITY_ROLE) {
hub = IIdentityVerificationHubV1(hubAddress); hub = IIdentityVerificationHubV1(hubAddress);
} }
/// @notice Updates the registry contract address /// @notice Updates the registry contract address
/// @param registryAddress The new registry contract address /// @param registryAddress The new registry contract address
/// @dev Only callable by the contract owner /// @dev Only callable by accounts with SECURITY_ROLE
function setRegistry(address registryAddress) external onlyOwner { function setRegistry(address registryAddress) external onlyRole(SECURITY_ROLE) {
registry = IIdentityRegistryV1(registryAddress); registry = IIdentityRegistryV1(registryAddress);
} }
} }

View 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;
}
}

View 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 {}
}

View 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];
}
}

View 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);
}
}

View 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];
}
}

View File

@@ -4,15 +4,23 @@ pragma solidity 0.8.28;
import {ImplRoot} from "../../contracts/upgradeable/ImplRoot.sol"; import {ImplRoot} from "../../contracts/upgradeable/ImplRoot.sol";
contract MockImplRoot is ImplRoot { contract MockImplRoot is ImplRoot {
function exposed__ImplRoot_init() external { function exposed__ImplRoot_init() external initializer {
__ImplRoot_init(); __ImplRoot_init();
} }
function exposed__Ownable_init(address initialOwner) external initializer {
__Ownable_init(initialOwner);
}
function exposed_authorizeUpgrade(address newImplementation) external { function exposed_authorizeUpgrade(address newImplementation) external {
_authorizeUpgrade(newImplementation); _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);
}
} }

View File

@@ -47,7 +47,7 @@ contract testUpgradedIdentityVerificationHubImplV1 is
* @param isTestInput Boolean value which shows it is test or not * @param isTestInput Boolean value which shows it is test or not
*/ */
function initialize(bool isTestInput) external reinitializer(3) { function initialize(bool isTestInput) external reinitializer(3) {
__ImplRoot_init(); __Ownable_init(msg.sender);
_isTest = isTestInput; _isTest = isTestInput;
emit TestHubInitialized(); emit TestHubInitialized();
} }

View File

@@ -2,15 +2,25 @@
pragma solidity 0.8.28; pragma solidity 0.8.28;
import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; 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 * @title ImplRoot
* @dev Abstract contract providing upgradeable functionality via UUPSUpgradeable, * @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. * 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. // Reserved storage space to allow for layout changes in the future.
uint256[50] private __gap; 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. * This function should be called in the initializer of the derived contract.
*/ */
function __ImplRoot_init() internal virtual onlyInitializing { function __ImplRoot_init() internal virtual onlyInitializing {
__Ownable_init(msg.sender); __AccessControl_init();
__UUPSUpgradeable_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. * @dev Authorizes an upgrade to a new implementation.
* Requirements: * Requirements:
* - Must be called through a proxy. * - 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. * @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) {}
} }

View File

@@ -1,18 +1,32 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
pragma solidity 0.8.28; pragma solidity 0.8.28;
import "@openzeppelin/contracts/access/Ownable.sol"; import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
/** /**
* @title PCR0Manager * @title PCR0Manager
* @notice This contract manages a mapping of PCR0 values (provided as a 48-byte value) * @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 * to booleans. The PCR0 value (the 48-byte SHA384 output) is hashed
* using keccak256 and then stored in the mapping. * 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 { contract PCR0Manager is AccessControl {
// Pass msg.sender directly to Ownable constructor /// @notice Critical operations and role management requiring 3/5 multisig consensus
constructor() Ownable(msg.sender) {} 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 from keccak256(pcr0) to its boolean state.
mapping(bytes32 => bool) public pcr0Mapping; 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. * @notice Adds a new PCR0 entry by setting its value to true.
* @param pcr0 The PCR0 value (must be exactly 48 bytes). * @param pcr0 The PCR0 value (must be exactly 32 bytes).
* @dev Reverts if the PCR0 value is not 48 bytes or if it is already set. * @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 { function addPCR0(bytes calldata pcr0) external onlyRole(SECURITY_ROLE) {
require(pcr0.length == 48, "PCR0 must be 48 bytes"); require(pcr0.length == 32, "PCR0 must be 32 bytes");
bytes32 key = keccak256(pcr0); bytes memory paddedPcr0 = abi.encodePacked(new bytes(16), pcr0);
bytes32 key = keccak256(paddedPcr0);
require(!pcr0Mapping[key], "PCR0 already set"); require(!pcr0Mapping[key], "PCR0 already set");
pcr0Mapping[key] = true; pcr0Mapping[key] = true;
emit PCR0Added(key); emit PCR0Added(key);
@@ -40,12 +56,14 @@ contract PCR0Manager is Ownable {
/** /**
* @notice Removes an existing PCR0 entry by setting its value to false. * @notice Removes an existing PCR0 entry by setting its value to false.
* @param pcr0 The PCR0 value (must be exactly 48 bytes). * @param pcr0 The PCR0 value (must be exactly 32 bytes).
* @dev Reverts if the PCR0 value is not 48 bytes or if it is not currently set. * @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 { function removePCR0(bytes calldata pcr0) external onlyRole(SECURITY_ROLE) {
require(pcr0.length == 48, "PCR0 must be 48 bytes"); require(pcr0.length == 32, "PCR0 must be 32 bytes");
bytes32 key = keccak256(pcr0); bytes memory paddedPcr0 = abi.encodePacked(new bytes(16), pcr0);
bytes32 key = keccak256(paddedPcr0);
require(pcr0Mapping[key], "PCR0 not set"); require(pcr0Mapping[key], "PCR0 not set");
pcr0Mapping[key] = false; pcr0Mapping[key] = false;
emit PCR0Removed(key); 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. * @notice Checks whether a given PCR0 value is set to true in the mapping.
* @param pcr0 The PCR0 value (must be exactly 48 bytes). * @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. * @return exists True if the PCR0 entry is set, false otherwise.
*/ */
function isPCR0Set(bytes calldata pcr0) external view returns (bool exists) { function isPCR0Set(bytes calldata pcr0) external view returns (bool exists) {

View File

@@ -37,25 +37,25 @@ contract Verifier_register_aadhaar {
uint256 constant gammax2 = 10857046999023057135944570762232829481370756359578518086990519993285655852781; uint256 constant gammax2 = 10857046999023057135944570762232829481370756359578518086990519993285655852781;
uint256 constant gammay1 = 4082367875863433681332203403145435568316851327593401208105741076214120093531; uint256 constant gammay1 = 4082367875863433681332203403145435568316851327593401208105741076214120093531;
uint256 constant gammay2 = 8495653923123431417604973247489272438418190587263600148770280649306958101930; uint256 constant gammay2 = 8495653923123431417604973247489272438418190587263600148770280649306958101930;
uint256 constant deltax1 = 1184175006002790631176821634090938467107330227007158853824891629496015889924; uint256 constant deltax1 = 3953219198104570901098823830840773856017689139278458081183220490752145815050;
uint256 constant deltax2 = 12086636205582787465813058141825079064824697543086779109775595053805081617827; uint256 constant deltax2 = 428186582661072144108009098107578252463491462238432931497262180014713596115;
uint256 constant deltay1 = 4456837667197728326322115376478122146150647259307011732553476664405503785753; uint256 constant deltay1 = 18162968189172780580333095558539690618880186036957545736311404283407493778880;
uint256 constant deltay2 = 9088696651190771223855139438876954166862164661620992858425695135876196457926; uint256 constant deltay2 = 7343682947937413219111184190299798376421430633278379635221606345245958931239;
uint256 constant IC0x = 6547380589242664979389953612506618657067204598675122139604885565320676833158; uint256 constant IC0x = 18984838814932147425072354846429508676387686524229308734161716095463360490134;
uint256 constant IC0y = 19055399919951028177234969337049077818155869440497248883170998389487338107126; uint256 constant IC0y = 11220857659665071811279473081460089783437970319349511357818317231596300603739;
uint256 constant IC1x = 20557545828033851521979343305884318041481443328161582179150888164584749744669; uint256 constant IC1x = 20557545828033851521979343305884318041481443328161582179150888164584749744669;
uint256 constant IC1y = 21560118189953885636148717201222479281100786469743463492679572665614931385205; uint256 constant IC1y = 21560118189953885636148717201222479281100786469743463492679572665614931385205;
uint256 constant IC2x = 17559551632997878871440402139938294429514970824368869332125462241643052815376; uint256 constant IC2x = 16679737504993527028036863898232919844061144900682159566005073982271081014169;
uint256 constant IC2y = 18428425902276807983388946110037886804676016275275246286544615654725514849838; uint256 constant IC2y = 608284743266912406546568650108359232826801114144551290914053108404596136834;
uint256 constant IC3x = 18768989044514693938417600792629717603460465495191187242290958821278680606604; uint256 constant IC3x = 10860675204793746311158823740347274485680296774011483979610282012223174969615;
uint256 constant IC3y = 6584358559179261704032830455997936799129839324733806160004605275139747821694; uint256 constant IC3y = 15029247271645078761075880233744321449871203863194823447037797284218806524473;
uint256 constant IC4x = 16692378542219000347024593964346873649905710163948976095790586330709671710647; uint256 constant IC4x = 17894574390662839711557891944994831304061930223868407277717041388786423798517;
uint256 constant IC4y = 2622311591517607336391164955074698243697841582935873217110527812716210930596; uint256 constant IC4y = 10747262533817845366080322542335489573224940900925614580771284497946454436591;
// Memory data // Memory data
uint16 constant pVk = 0; uint16 constant pVk = 0;

View 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
View 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"

View File

@@ -1,12 +1,15 @@
import { HardhatUserConfig } from "hardhat/config"; import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox"; import "@nomicfoundation/hardhat-toolbox";
import "@openzeppelin/hardhat-upgrades";
import dotenv from "dotenv"; import dotenv from "dotenv";
dotenv.config(); dotenv.config();
import "hardhat-contract-sizer"; import "hardhat-contract-sizer";
import "@nomicfoundation/hardhat-ignition-ethers"; import "@nomicfoundation/hardhat-ignition-ethers";
import "solidity-coverage"; import "solidity-coverage";
import "hardhat-gas-reporter"; 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) // Use a dummy private key for CI/local development (not used for actual deployments)
const DUMMY_PRIVATE_KEY = "0x0000000000000000000000000000000000000000000000000000000000000001"; const DUMMY_PRIVATE_KEY = "0x0000000000000000000000000000000000000000000000000000000000000001";
@@ -16,9 +19,10 @@ const config: HardhatUserConfig = {
solidity: { solidity: {
version: "0.8.28", version: "0.8.28",
settings: { settings: {
evmVersion: "cancun",
optimizer: { optimizer: {
enabled: true, enabled: true,
runs: 100000, runs: 200,
}, },
}, },
}, },
@@ -37,7 +41,7 @@ const config: HardhatUserConfig = {
chainId: 31337, chainId: 31337,
url: "http://127.0.0.1:8545", url: "http://127.0.0.1:8545",
accounts: { 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, count: 20,
}, },
}, },

View File

@@ -71,7 +71,10 @@
"update:ofacroot": "npx dotenv-cli -- bash -c 'NETWORK=${NETWORK} npx tsx scripts/updateRegistryOfacRoot.ts'", "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'", "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: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": { "dependencies": {
"@ashpect/smt": "https://github.com/ashpect/smt#main", "@ashpect/smt": "https://github.com/ashpect/smt#main",
@@ -81,6 +84,9 @@
"@openpassport/zk-kit-smt": "^0.0.1", "@openpassport/zk-kit-smt": "^0.0.1",
"@openzeppelin/contracts": "5.4.0", "@openzeppelin/contracts": "5.4.0",
"@openzeppelin/contracts-upgradeable": "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:^", "@selfxyz/common": "workspace:^",
"@zk-kit/imt": "^2.0.0-beta.4", "@zk-kit/imt": "^2.0.0-beta.4",
"@zk-kit/imt.sol": "^2.0.0-beta.12", "@zk-kit/imt.sol": "^2.0.0-beta.12",
@@ -103,6 +109,7 @@
"@nomicfoundation/hardhat-toolbox": "^3.0.0", "@nomicfoundation/hardhat-toolbox": "^3.0.0",
"@nomicfoundation/hardhat-verify": "^2.0.6", "@nomicfoundation/hardhat-verify": "^2.0.6",
"@nomicfoundation/ignition-core": "^0.15.12", "@nomicfoundation/ignition-core": "^0.15.12",
"@openzeppelin/hardhat-upgrades": "^3.9.1",
"@typechain/ethers-v6": "^0.4.3", "@typechain/ethers-v6": "^0.4.3",
"@typechain/hardhat": "^8.0.3", "@typechain/hardhat": "^8.0.3",
"@types/chai": "^4.3.16", "@types/chai": "^4.3.16",

4
contracts/remappings.txt Normal file
View 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/

View 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!");
}
}

View 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 |

View 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 {};

View 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 {};

View 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 {};

View 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 {};

View 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 {};

View 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";

File diff suppressed because it is too large Load Diff

View 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;
}

View 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("================================================================================");
}
}

View 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("================================================================================");
}
}

View File

@@ -2,27 +2,55 @@ import { expect } from "chai";
import { BigNumberish, TransactionReceipt } from "ethers"; import { BigNumberish, TransactionReceipt } from "ethers";
import { ethers } from "hardhat"; import { ethers } from "hardhat";
import { poseidon2 } from "poseidon-lite"; import { poseidon2 } from "poseidon-lite";
import { createHash } from "crypto";
import { CIRCUIT_CONSTANTS, DscVerifierId, RegisterVerifierId } from "@selfxyz/common/constants/constants"; import { CIRCUIT_CONSTANTS, DscVerifierId, RegisterVerifierId } from "@selfxyz/common/constants/constants";
import { formatCountriesList, reverseBytes } from "@selfxyz/common/utils/circuits/formatInputs"; import { formatCountriesList, reverseBytes } from "@selfxyz/common/utils/circuits/formatInputs";
import { castFromScope } from "@selfxyz/common/utils/circuits/uuid"; import { castFromScope } from "@selfxyz/common/utils/circuits/uuid";
import { ATTESTATION_ID } from "../utils/constants"; import { ATTESTATION_ID } from "../utils/constants";
import { deploySystemFixtures } from "../utils/deployment"; import { deploySystemFixturesV2 } from "../utils/deploymentV2";
import BalanceTree from "../utils/example/balance-tree"; import BalanceTree from "../utils/example/balance-tree";
import { Formatter } from "../utils/formatter"; import { Formatter } from "../utils/formatter";
import { generateDscProof, generateRegisterProof, generateVcAndDiscloseProof } from "../utils/generateProof"; 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 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"; 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 () { describe("End to End Tests", function () {
this.timeout(0); this.timeout(0);
let deployedActors: DeployedActors; let deployedActors: DeployedActorsV2;
let snapshotId: string; let snapshotId: string;
before(async () => { before(async () => {
deployedActors = await deploySystemFixtures(); deployedActors = await deploySystemFixturesV2();
snapshotId = await ethers.provider.send("evm_snapshot", []); 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 () => { 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 // register dsc key
// To increase test performance, we will just set one dsc key with groth16 proof // 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]) { if (BigInt(dscKeys[0][i]) == dscProof.pubSignals[CIRCUIT_CONSTANTS.DSC_TREE_LEAF_INDEX]) {
const previousRoot = await registry.getDscKeyCommitmentMerkleRoot(); const previousRoot = await registry.getDscKeyCommitmentMerkleRoot();
const previousSize = await registry.getDscKeyCommitmentTreeSize(); 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 receipt = (await registerDscTx.wait()) as TransactionReceipt;
const event = receipt?.logs.find( const event = receipt?.logs.find(
(log) => log.topics[0] === registry.interface.getEvent("DscKeyCommitmentRegistered").topicHash, (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); const imt = new LeanIMT<bigint>(hashFunction);
await imt.insert(BigInt(registerProof.pubSignals[CIRCUIT_CONSTANTS.REGISTER_COMMITMENT_INDEX])); 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, RegisterVerifierId.register_sha256_sha256_sha256_rsa_65537_4096,
registerProof, registerProof,
); );
@@ -104,7 +140,7 @@ describe("End to End Tests", function () {
registerProof.pubSignals[CIRCUIT_CONSTANTS.REGISTER_COMMITMENT_INDEX], registerProof.pubSignals[CIRCUIT_CONSTANTS.REGISTER_COMMITMENT_INDEX],
); );
const identityNullifier = await registry.nullifiers( const identityNullifier = await registry.nullifiers(
ATTESTATION_ID.E_PASSPORT, attestationIdBytes32,
registerProof.pubSignals[CIRCUIT_CONSTANTS.REGISTER_NULLIFIER_INDEX], registerProof.pubSignals[CIRCUIT_CONSTANTS.REGISTER_NULLIFIER_INDEX],
); );
@@ -134,11 +170,25 @@ describe("End to End Tests", function () {
reverseBytes(Formatter.bytesToHexString(new Uint8Array(formatCountriesList(forbiddenCountriesList)))), 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( const vcAndDiscloseProof = await generateVcAndDiscloseProof(
registerSecret, registerSecret,
BigInt(ATTESTATION_ID.E_PASSPORT).toString(), BigInt(ATTESTATION_ID.E_PASSPORT).toString(),
mockPassport, mockPassport,
"test-scope", testRootScope.toString(),
new Array(88).fill("1"), new Array(88).fill("1"),
"1", "1",
imt, imt,
@@ -148,59 +198,114 @@ describe("End to End Tests", function () {
undefined, undefined,
undefined, undefined,
forbiddenCountriesList, forbiddenCountriesList,
(await user1.getAddress()).slice(2), userIdentifierHash,
); );
const vcAndDiscloseHubProof: VcAndDiscloseHubProof = { // Set up verification config for testSelfVerificationRoot
const verificationConfigV2 = {
olderThanEnabled: true, olderThanEnabled: true,
olderThan: "20", olderThan: "20",
forbiddenCountriesEnabled: true, forbiddenCountriesEnabled: true,
forbiddenCountriesListPacked: countriesListPacked, forbiddenCountriesListPacked: countriesListPacked as [BigNumberish, BigNumberish, BigNumberish, BigNumberish],
ofacEnabled: [true, true, true] as [boolean, boolean, boolean], 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( // Create V2 proof format and verify via testSelfVerificationRoot
vcAndDiscloseProof.pubSignals[CIRCUIT_CONSTANTS.VC_AND_DISCLOSE_MERKLE_ROOT_INDEX], 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]); // Reset test state before verification
expect(result.attestationId).to.equal( await testSelfVerificationRoot.resetTestState();
vcAndDiscloseProof.pubSignals[CIRCUIT_CONSTANTS.VC_AND_DISCLOSE_ATTESTATION_ID_INDEX],
// 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++) { // Verify userIdentifier is set
expect(result.forbiddenCountriesListPacked[i]).to.equal(BigInt(countriesListPacked[i])); expect(lastOutput.userIdentifier).to.not.equal(0n);
}
// Verify olderThan value
expect(lastOutput.olderThan).to.equal(20n);
const tokenFactory = await ethers.getContractFactory("AirdropToken"); const tokenFactory = await ethers.getContractFactory("AirdropToken");
const token = await tokenFactory.connect(owner).deploy(); const token = await tokenFactory.connect(owner).deploy();
await token.waitForDeployment(); await token.waitForDeployment();
const airdropFactory = await ethers.getContractFactory("Airdrop"); const airdropFactory = await ethers.getContractFactory("Airdrop");
const airdrop = await airdropFactory.connect(owner).deploy( const airdrop = await airdropFactory.connect(owner).deploy(hub.target, "test-scope", token.target);
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],
);
await airdrop.waitForDeployment(); 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)); 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(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(); await airdrop.connect(owner).closeRegistration();
const tree = new BalanceTree([{ account: await user1.getAddress(), amount: BigInt(1000000000000000000) }]); 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()); const isClaimed = await airdrop.claimed(await user1.getAddress());
expect(isClaimed).to.be.true; expect(isClaimed).to.be.true;
const readableData = await hub.getReadableRevealedData( // Verify disclosed attributes from lastOutput
[result.revealedDataPacked[0], result.revealedDataPacked[1], result.revealedDataPacked[2]], expect(lastOutput.issuingState).to.equal("FRA");
["0", "1", "2", "3", "4", "5", "6", "7", "8"], expect(lastOutput.idNumber).to.equal("15AA81234");
); expect(lastOutput.nationality).to.equal("FRA");
expect(lastOutput.dateOfBirth).to.equal("31-01-94");
expect(readableData[0]).to.equal("FRA"); expect(lastOutput.gender).to.equal("M");
expect(readableData[1]).to.deep.equal(["ALPHONSE HUGHUES ALBERT", "DUPONT"]); expect(lastOutput.expiryDate).to.equal("31-10-40");
expect(readableData[2]).to.equal("15AA81234"); expect(lastOutput.olderThan).to.equal(20n);
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);
}); });
}); });

View File

@@ -11,8 +11,9 @@ import { BigNumberish } from "ethers";
import { generateRandomFieldElement, getStartOfDayTimestamp, splitHexFromBack } from "../utils/utils"; import { generateRandomFieldElement, getStartOfDayTimestamp, splitHexFromBack } from "../utils/utils";
import { Formatter, CircuitAttributeHandler } from "../utils/formatter"; import { Formatter, CircuitAttributeHandler } from "../utils/formatter";
import { formatCountriesList, reverseBytes, reverseCountryBytes } from "@selfxyz/common/utils/circuits/formatInputs"; 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 { countries, Country3LetterCode } from "@selfxyz/common/constants/countries";
import { castFromScope } from "@selfxyz/common/utils/circuits/uuid";
import path from "path"; import path from "path";
describe("VC and Disclose", () => { describe("VC and Disclose", () => {
@@ -100,7 +101,7 @@ describe("VC and Disclose", () => {
registerSecret, registerSecret,
BigInt(ATTESTATION_ID.E_PASSPORT).toString(), BigInt(ATTESTATION_ID.E_PASSPORT).toString(),
deployedActors.mockPassport, deployedActors.mockPassport,
"test-scope", castFromScope("test-scope"),
new Array(88).fill("1"), new Array(88).fill("1"),
"1", "1",
imt, imt,
@@ -110,7 +111,7 @@ describe("VC and Disclose", () => {
undefined, undefined,
undefined, undefined,
forbiddenCountriesList, forbiddenCountriesList,
(await deployedActors.user1.getAddress()).slice(2), await deployedActors.user1.getAddress(),
); );
snapshotId = await ethers.provider.send("evm_snapshot", []); snapshotId = await ethers.provider.send("evm_snapshot", []);
}); });
@@ -439,6 +440,7 @@ describe("VC and Disclose", () => {
const { hub, registry, owner, mockPassport } = deployedActors; const { hub, registry, owner, mockPassport } = deployedActors;
const hashFunction = (a: bigint, b: bigint) => poseidon2([a, b]); 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); const imt = new LeanIMT<bigint>(hashFunction);
imt.insert(BigInt(commitment)); imt.insert(BigInt(commitment));
@@ -448,7 +450,7 @@ describe("VC and Disclose", () => {
registerSecret, registerSecret,
BigInt(ATTESTATION_ID.E_PASSPORT).toString(), BigInt(ATTESTATION_ID.E_PASSPORT).toString(),
mockPassport, mockPassport,
"test-scope", castFromScope("test-scope"),
new Array(88).fill("1"), new Array(88).fill("1"),
"1", "1",
imt, imt,
@@ -746,43 +748,60 @@ describe("VC and Disclose", () => {
it("should parse forbidden countries with CircuitAttributeHandler", async () => { it("should parse forbidden countries with CircuitAttributeHandler", async () => {
const { hub } = deployedActors; const { hub } = deployedActors;
const forbiddenCountriesListPacked = splitHexFromBack( const localForbiddenCountriesList = ["AFG", "ABC", "CBA"] as const;
reverseCountryBytes(Formatter.bytesToHexString(new Uint8Array(formatCountriesList(forbiddenCountriesList)))), const forbiddenCountriesListPacked = getPackedForbiddenCountries([...localForbiddenCountriesList]);
);
const readableForbiddenCountries = await hub.getReadableForbiddenCountries(forbiddenCountriesListPacked); const readableForbiddenCountries = await hub.getReadableForbiddenCountries(forbiddenCountriesListPacked);
expect(readableForbiddenCountries[0]).to.equal(forbiddenCountriesList[0]); expect(readableForbiddenCountries[0]).to.equal(localForbiddenCountriesList[0]);
expect(readableForbiddenCountries[1]).to.equal(forbiddenCountriesList[1]); expect(readableForbiddenCountries[1]).to.equal(localForbiddenCountriesList[1]);
expect(readableForbiddenCountries[2]).to.equal(forbiddenCountriesList[2]); expect(readableForbiddenCountries[2]).to.equal(localForbiddenCountriesList[2]);
}); });
it("should return maximum length of forbidden countries", async () => { it("should return maximum length of forbidden countries", async () => {
const { hub } = deployedActors; const { hub } = deployedActors;
const forbiddenCountriesList = ["AAA", "FRA", "CBA", "CBA", "CBA", "CBA", "CBA", "CBA", "CBA", "CBA"]; const localForbiddenCountriesList = [
const forbiddenCountriesListPacked = splitHexFromBack( "AAA",
reverseCountryBytes(Formatter.bytesToHexString(new Uint8Array(formatCountriesList(forbiddenCountriesList)))), "FRA",
); "CBA",
"CBA",
"CBA",
"CBA",
"CBA",
"CBA",
"CBA",
"CBA",
] as const;
const forbiddenCountriesListPacked = getPackedForbiddenCountries([...localForbiddenCountriesList]);
const readableForbiddenCountries = await hub.getReadableForbiddenCountries(forbiddenCountriesListPacked); const readableForbiddenCountries = await hub.getReadableForbiddenCountries(forbiddenCountriesListPacked);
expect(readableForbiddenCountries.length).to.equal(40); expect(readableForbiddenCountries.length).to.equal(40);
expect(readableForbiddenCountries[0]).to.equal(forbiddenCountriesList[0]); expect(readableForbiddenCountries[0]).to.equal(localForbiddenCountriesList[0]);
expect(readableForbiddenCountries[1]).to.equal(forbiddenCountriesList[1]); expect(readableForbiddenCountries[1]).to.equal(localForbiddenCountriesList[1]);
expect(readableForbiddenCountries[2]).to.equal(forbiddenCountriesList[2]); expect(readableForbiddenCountries[2]).to.equal(localForbiddenCountriesList[2]);
expect(readableForbiddenCountries[3]).to.equal(forbiddenCountriesList[3]); expect(readableForbiddenCountries[3]).to.equal(localForbiddenCountriesList[3]);
expect(readableForbiddenCountries[4]).to.equal(forbiddenCountriesList[4]); expect(readableForbiddenCountries[4]).to.equal(localForbiddenCountriesList[4]);
expect(readableForbiddenCountries[5]).to.equal(forbiddenCountriesList[5]); expect(readableForbiddenCountries[5]).to.equal(localForbiddenCountriesList[5]);
expect(readableForbiddenCountries[6]).to.equal(forbiddenCountriesList[6]); expect(readableForbiddenCountries[6]).to.equal(localForbiddenCountriesList[6]);
expect(readableForbiddenCountries[7]).to.equal(forbiddenCountriesList[7]); expect(readableForbiddenCountries[7]).to.equal(localForbiddenCountriesList[7]);
expect(readableForbiddenCountries[8]).to.equal(forbiddenCountriesList[8]); expect(readableForbiddenCountries[8]).to.equal(localForbiddenCountriesList[8]);
expect(readableForbiddenCountries[9]).to.equal(forbiddenCountriesList[9]); expect(readableForbiddenCountries[9]).to.equal(localForbiddenCountriesList[9]);
}); });
it("should fail when getReadableForbiddenCountries is called by non-proxy", async () => { it("should fail when getReadableForbiddenCountries is called by non-proxy", async () => {
const { hubImpl } = deployedActors; const { hubImpl } = deployedActors;
const forbiddenCountriesList = ["AAA", "FRA", "CBA", "CBA", "CBA", "CBA", "CBA", "CBA", "CBA", "CBA"]; const localForbiddenCountriesList = [
const forbiddenCountriesListPacked = splitHexFromBack( "AAA",
reverseCountryBytes(Formatter.bytesToHexString(new Uint8Array(formatCountriesList(forbiddenCountriesList)))), "FRA",
); "CBA",
"CBA",
"CBA",
"CBA",
"CBA",
"CBA",
"CBA",
"CBA",
] as const;
const forbiddenCountriesListPacked = getPackedForbiddenCountries([...localForbiddenCountriesList]);
await expect(hubImpl.getReadableForbiddenCountries(forbiddenCountriesListPacked)).to.be.revertedWithCustomError( await expect(hubImpl.getReadableForbiddenCountries(forbiddenCountriesListPacked)).to.be.revertedWithCustomError(
hubImpl, hubImpl,
"UUPSUnauthorizedCallContext", "UUPSUnauthorizedCallContext",

View File

@@ -10,6 +10,7 @@ import { poseidon2 } from "poseidon-lite";
import { generateVcAndDiscloseProof, parseSolidityCalldata } from "../utils/generateProof"; import { generateVcAndDiscloseProof, parseSolidityCalldata } from "../utils/generateProof";
import { Formatter } from "../utils/formatter"; import { Formatter } from "../utils/formatter";
import { formatCountriesList, reverseBytes } from "@selfxyz/common/utils/circuits/formatInputs"; import { formatCountriesList, reverseBytes } from "@selfxyz/common/utils/circuits/formatInputs";
import { stringToBigInt } from "@selfxyz/common/utils/scope";
import { VerifyAll } from "../../typechain-types"; import { VerifyAll } from "../../typechain-types";
import { getSMTs } from "../utils/generateProof"; import { getSMTs } from "../utils/generateProof";
import { Groth16Proof, PublicSignals, groth16 } from "snarkjs"; import { Groth16Proof, PublicSignals, groth16 } from "snarkjs";
@@ -102,7 +103,7 @@ describe("VerifyAll", () => {
registerSecret, registerSecret,
BigInt(ATTESTATION_ID.E_PASSPORT).toString(), BigInt(ATTESTATION_ID.E_PASSPORT).toString(),
deployedActors.mockPassport, deployedActors.mockPassport,
"test-scope", stringToBigInt("test-scope").toString(),
new Array(88).fill("1"), new Array(88).fill("1"),
"1", "1",
imt, imt,
@@ -112,7 +113,7 @@ describe("VerifyAll", () => {
undefined, undefined,
undefined, undefined,
forbiddenCountriesList, forbiddenCountriesList,
(await deployedActors.user1.getAddress()).slice(2), await deployedActors.user1.getAddress(),
); );
snapshotId = await ethers.provider.send("evm_snapshot", []); snapshotId = await ethers.provider.send("evm_snapshot", []);
}); });
@@ -293,7 +294,7 @@ describe("VerifyAll", () => {
registerSecret, registerSecret,
BigInt(ATTESTATION_ID.E_PASSPORT).toString(), BigInt(ATTESTATION_ID.E_PASSPORT).toString(),
deployedActors.mockPassport, deployedActors.mockPassport,
"test-scope", stringToBigInt("test-scope").toString(),
new Array(88).fill("1"), new Array(88).fill("1"),
"1", "1",
imt, imt,
@@ -460,7 +461,7 @@ describe("VerifyAll", () => {
const newHubAddress = await deployedActors.user1.getAddress(); const newHubAddress = await deployedActors.user1.getAddress();
await expect(verifyAll.connect(deployedActors.user1).setHub(newHubAddress)).to.be.revertedWithCustomError( await expect(verifyAll.connect(deployedActors.user1).setHub(newHubAddress)).to.be.revertedWithCustomError(
verifyAll, verifyAll,
"OwnableUnauthorizedAccount", "AccessControlUnauthorizedAccount",
); );
}); });
@@ -468,7 +469,7 @@ describe("VerifyAll", () => {
const newRegistryAddress = await deployedActors.user1.getAddress(); const newRegistryAddress = await deployedActors.user1.getAddress();
await expect( await expect(
verifyAll.connect(deployedActors.user1).setRegistry(newRegistryAddress), verifyAll.connect(deployedActors.user1).setRegistry(newRegistryAddress),
).to.be.revertedWithCustomError(verifyAll, "OwnableUnauthorizedAccount"); ).to.be.revertedWithCustomError(verifyAll, "AccessControlUnauthorizedAccount");
}); });
}); });

View File

@@ -357,7 +357,7 @@ describe("Unit Tests for IdentityRegistry", () => {
await expect(registry.connect(user1).updateHub(newHubAddress)).to.be.revertedWithCustomError( await expect(registry.connect(user1).updateHub(newHubAddress)).to.be.revertedWithCustomError(
registry, registry,
"OwnableUnauthorizedAccount", "AccessControlUnauthorizedAccount",
); );
}); });
@@ -394,7 +394,7 @@ describe("Unit Tests for IdentityRegistry", () => {
expect(await registry.getNameAndYobOfacRoot()).to.equal(yobRoot); 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 { registry, user1 } = deployedActors;
const passportRoot = generateRandomFieldElement(); const passportRoot = generateRandomFieldElement();
const dobRoot = generateRandomFieldElement(); const dobRoot = generateRandomFieldElement();
@@ -402,15 +402,15 @@ describe("Unit Tests for IdentityRegistry", () => {
await expect(registry.connect(user1).updatePassportNoOfacRoot(passportRoot)).to.be.revertedWithCustomError( await expect(registry.connect(user1).updatePassportNoOfacRoot(passportRoot)).to.be.revertedWithCustomError(
registry, registry,
"OwnableUnauthorizedAccount", "AccessControlUnauthorizedAccount",
); );
await expect(registry.connect(user1).updateNameAndDobOfacRoot(dobRoot)).to.be.revertedWithCustomError( await expect(registry.connect(user1).updateNameAndDobOfacRoot(dobRoot)).to.be.revertedWithCustomError(
registry, registry,
"OwnableUnauthorizedAccount", "AccessControlUnauthorizedAccount",
); );
await expect(registry.connect(user1).updateNameAndYobOfacRoot(yobRoot)).to.be.revertedWithCustomError( await expect(registry.connect(user1).updateNameAndYobOfacRoot(yobRoot)).to.be.revertedWithCustomError(
registry, registry,
"OwnableUnauthorizedAccount", "AccessControlUnauthorizedAccount",
); );
}); });
@@ -443,13 +443,13 @@ describe("Unit Tests for IdentityRegistry", () => {
expect(await registry.getCscaRoot()).to.equal(newCscaRoot); 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 { registry, user1 } = deployedActors;
const newCscaRoot = generateRandomFieldElement(); const newCscaRoot = generateRandomFieldElement();
await expect(registry.connect(user1).updateCscaRoot(newCscaRoot)).to.be.revertedWithCustomError( await expect(registry.connect(user1).updateCscaRoot(newCscaRoot)).to.be.revertedWithCustomError(
registry, registry,
"OwnableUnauthorizedAccount", "AccessControlUnauthorizedAccount",
); );
}); });
@@ -498,7 +498,7 @@ describe("Unit Tests for IdentityRegistry", () => {
await expect( await expect(
registry.connect(user1).devAddIdentityCommitment(attestationId, nullifier, commitment), 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 () => { it("should not add commitment if caller is not proxy", async () => {
@@ -546,7 +546,7 @@ describe("Unit Tests for IdentityRegistry", () => {
const newCommitment = generateRandomFieldElement(); const newCommitment = generateRandomFieldElement();
await expect( await expect(
registry.connect(user1).devUpdateCommitment(commitment, newCommitment, []), 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 () => { 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 registry.devAddIdentityCommitment(attestationId, nullifier, commitment);
await expect(registry.connect(user1).devRemoveCommitment(commitment, [])).to.be.revertedWithCustomError( await expect(registry.connect(user1).devRemoveCommitment(commitment, [])).to.be.revertedWithCustomError(
registry, registry,
"OwnableUnauthorizedAccount", "AccessControlUnauthorizedAccount",
); );
}); });
@@ -632,7 +632,7 @@ describe("Unit Tests for IdentityRegistry", () => {
const dscCommitment = generateRandomFieldElement(); const dscCommitment = generateRandomFieldElement();
await expect(registry.connect(user1).devAddDscKeyCommitment(dscCommitment)).to.be.revertedWithCustomError( await expect(registry.connect(user1).devAddDscKeyCommitment(dscCommitment)).to.be.revertedWithCustomError(
registry, registry,
"OwnableUnauthorizedAccount", "AccessControlUnauthorizedAccount",
); );
}); });
@@ -673,7 +673,7 @@ describe("Unit Tests for IdentityRegistry", () => {
await registry.devAddDscKeyCommitment(dscCommitment); await registry.devAddDscKeyCommitment(dscCommitment);
await expect( await expect(
registry.connect(user1).devUpdateDscKeyCommitment(dscCommitment, newDscCommitment, []), 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 () => { 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 registry.devAddDscKeyCommitment(dscCommitment);
await expect(registry.connect(user1).devRemoveDscKeyCommitment(dscCommitment, [])).to.be.revertedWithCustomError( await expect(registry.connect(user1).devRemoveDscKeyCommitment(dscCommitment, [])).to.be.revertedWithCustomError(
registry, registry,
"OwnableUnauthorizedAccount", "AccessControlUnauthorizedAccount",
); );
}); });
@@ -751,7 +751,7 @@ describe("Unit Tests for IdentityRegistry", () => {
const nullifier = generateRandomFieldElement(); const nullifier = generateRandomFieldElement();
await expect( await expect(
registry.connect(user1).devChangeNullifierState(attestationId, nullifier, false), 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 () => { it("should not change nullifier state if caller is not proxy", async () => {
@@ -789,7 +789,7 @@ describe("Unit Tests for IdentityRegistry", () => {
const state = true; const state = true;
await expect( await expect(
registry.connect(user1).devChangeDscKeyCommitmentState(dscCommitment, state), 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 () => { 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( await expect(
registry.connect(user1).upgradeToAndCall(registryV2Implementation.target, "0x"), 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 () => { it("should not allow implementation contract to be initialized directly", async () => {

View File

@@ -1,95 +1,272 @@
import { expect } from "chai"; import { expect } from "chai";
import { ethers } from "hardhat"; import { ethers } from "hardhat";
import { ZeroAddress } from "ethers";
import { MockImplRoot } from "../../typechain-types"; import { MockImplRoot } from "../../typechain-types";
import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers";
describe("ImplRoot", () => { describe("ImplRoot", () => {
let mockImplRoot: MockImplRoot; let mockImplRoot: MockImplRoot;
let owner: any; let deployer: SignerWithAddress;
let user1: any; 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 () => { 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(); mockImplRoot = await MockImplRootFactory.deploy();
await mockImplRoot.waitForDeployment(); 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", () => { describe("Initialization", () => {
it("should revert when calling __ImplRoot_init outside initialization phase", async () => { 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( 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, mockImplRoot,
"InvalidInitialization", "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", () => { describe("Upgrade Authorization", () => {
let proxy: any; let initializedContract: MockImplRoot;
let implContract: any;
beforeEach(async () => { beforeEach(async () => {
const MockImplRootFactory = await ethers.getContractFactory("MockImplRoot", owner); const MockImplRootFactory = await ethers.getContractFactory("MockImplRoot");
implContract = await MockImplRootFactory.deploy(); initializedContract = await MockImplRootFactory.deploy();
await implContract.waitForDeployment(); await initializedContract.waitForDeployment();
const initData = implContract.interface.encodeFunctionData("exposed__Ownable_init", [owner.address]); // Initialize and transfer roles
await initializedContract.exposed__ImplRoot_init();
const ProxyFactory = await ethers.getContractFactory("ERC1967Proxy"); await initializedContract.connect(deployer).grantRole(SECURITY_ROLE, securityMultisig.address);
proxy = await ProxyFactory.deploy(implContract.target, initData); await initializedContract.connect(deployer).grantRole(OPERATIONS_ROLE, operationsMultisig.address);
await proxy.waitForDeployment();
mockImplRoot = await ethers.getContractAt("MockImplRoot", proxy.target);
}); });
it("should revert when calling _authorizeUpgrade from non-proxy", async () => { it("should allow critical multisig to authorize upgrades", async () => {
const MockImplRootFactory = await ethers.getContractFactory("MockImplRoot", owner); const newImplementation = ethers.Wallet.createRandom().address;
const newImpl = await MockImplRootFactory.deploy();
await newImpl.waitForDeployment();
await expect(implContract.exposed_authorizeUpgrade(newImpl.target)).to.be.revertedWithCustomError( // Note: _authorizeUpgrade is internal and can only be called through proxy upgrade mechanism
implContract, // We test this by verifying the critical multisig has the required role
"UUPSUnauthorizedCallContext", 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 () => { // 4. Verify multisigs can operate (check role permissions)
const MockImplRootFactory = await ethers.getContractFactory("MockImplRoot", owner); expect(await contract.hasRole(SECURITY_ROLE, securityMultisig.address)).to.be.true;
const newImpl = await MockImplRootFactory.deploy();
await newImpl.waitForDeployment();
await expect(mockImplRoot.connect(user1).exposed_authorizeUpgrade(newImpl.target)) console.log("✅ Step 4: Multisigs verified functional");
.to.be.revertedWithCustomError(mockImplRoot, "OwnableUnauthorizedAccount")
.withArgs(user1.address);
});
it("should allow owner to call _authorizeUpgrade through proxy", async () => { // 5. Renounce deployer roles
const MockImplRootFactory = await ethers.getContractFactory("MockImplRoot", owner); await contract.connect(deployer).renounceRole(SECURITY_ROLE, deployer.address);
const newImpl = await MockImplRootFactory.deploy(); await contract.connect(deployer).renounceRole(OPERATIONS_ROLE, deployer.address);
await newImpl.waitForDeployment();
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!");
}); });
}); });
}); });

View File

@@ -8,9 +8,14 @@ describe("PCR0Manager", function () {
let owner: SignerWithAddress; let owner: SignerWithAddress;
let other: SignerWithAddress; let other: SignerWithAddress;
// Sample PCR0 value for testing (48 bytes) // Sample PCR0 value for testing
const samplePCR0 = "0x" + "00".repeat(48); // addPCR0/removePCR0 expect 32 bytes (GCP image hash)
const invalidPCR0 = "0x" + "00".repeat(32); // 32 bytes (invalid size) 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 () { beforeEach(async function () {
[owner, other] = await ethers.getSigners(); [owner, other] = await ethers.getSigners();
@@ -21,74 +26,68 @@ describe("PCR0Manager", function () {
describe("addPCR0", function () { describe("addPCR0", function () {
it("should allow owner to add PCR0 value", async 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; expect(await pcr0Manager.isPCR0Set(samplePCR0_48bytes)).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;
}); });
it("should not allow non-owner to add PCR0 value", async function () { it("should not allow non-owner to add PCR0 value", async function () {
await expect(pcr0Manager.connect(other).addPCR0(samplePCR0)) await expect(pcr0Manager.connect(other).addPCR0(samplePCR0_32bytes))
.to.be.revertedWithCustomError(pcr0Manager, "OwnableUnauthorizedAccount") .to.be.revertedWithCustomError(pcr0Manager, "AccessControlUnauthorizedAccount")
.withArgs(other.address); .withArgs(other.address, await pcr0Manager.SECURITY_ROLE());
}); });
it("should not allow adding PCR0 with invalid size", async function () { 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 () { it("should not allow adding duplicate PCR0", async function () {
await pcr0Manager.addPCR0(samplePCR0); await pcr0Manager.addPCR0(samplePCR0_32bytes);
await expect(pcr0Manager.addPCR0(samplePCR0)).to.be.revertedWith("PCR0 already set"); await expect(pcr0Manager.addPCR0(samplePCR0_32bytes)).to.be.revertedWith("PCR0 already set");
}); });
}); });
describe("removePCR0", function () { describe("removePCR0", function () {
beforeEach(async function () { beforeEach(async function () {
await pcr0Manager.addPCR0(samplePCR0); await pcr0Manager.addPCR0(samplePCR0_32bytes);
}); });
it("should allow owner to remove PCR0 value", async function () { 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 // 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 () { 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 () { it("should not allow non-owner to remove PCR0 value", async function () {
await expect(pcr0Manager.connect(other).removePCR0(samplePCR0)) await expect(pcr0Manager.connect(other).removePCR0(samplePCR0_32bytes))
.to.be.revertedWithCustomError(pcr0Manager, "OwnableUnauthorizedAccount") .to.be.revertedWithCustomError(pcr0Manager, "AccessControlUnauthorizedAccount")
.withArgs(other.address); .withArgs(other.address, await pcr0Manager.SECURITY_ROLE());
}); });
it("should not allow removing non-existent PCR0", async function () { 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"); await expect(pcr0Manager.removePCR0(otherPCR0)).to.be.revertedWith("PCR0 not set");
}); });
}); });
describe("isPCR0Set", function () { describe("isPCR0Set", function () {
it("should correctly return PCR0 status", async 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); await pcr0Manager.addPCR0(samplePCR0_32bytes);
expect(await pcr0Manager.isPCR0Set(samplePCR0)).to.be.true; expect(await pcr0Manager.isPCR0Set(samplePCR0_48bytes)).to.be.true;
await pcr0Manager.removePCR0(samplePCR0); await pcr0Manager.removePCR0(samplePCR0_32bytes);
expect(await pcr0Manager.isPCR0Set(samplePCR0)).to.be.false; expect(await pcr0Manager.isPCR0Set(samplePCR0_48bytes)).to.be.false;
}); });
it("should not allow checking PCR0 with invalid size", async function () { 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");
}); });
}); });
}); });

View File

@@ -149,9 +149,11 @@ describe("Aadhaar Registration test", function () {
}); });
it("should not fail if timestamp is within 20 minutes", async () => { 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); await deployedActors.hub.setAadhaarRegistrationWindow(20);
const latestBlock = await ethers.provider.getBlock("latest");
const blockTimestamp = latestBlock!.timestamp;
const newAadhaarData = prepareAadhaarRegisterTestData( const newAadhaarData = prepareAadhaarRegisterTestData(
privateKeyPem, privateKeyPem,
pubkeyPem, pubkeyPem,
@@ -161,8 +163,7 @@ describe("Aadhaar Registration test", function () {
"M", "M",
"110051", "110051",
"WB", "WB",
//timestamp 10 minutes ago and converted to timestamp string (blockTimestamp - 10 * 60).toString(),
new Date(Date.now() - 10 * 60 * 1000).getTime().toString(),
); );
const newRegisterProof = await generateRegisterAadhaarProof(registerSecret, newAadhaarData.inputs); 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 () => { 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); await deployedActors.hub.setAadhaarRegistrationWindow(20);
const latestBlock = await ethers.provider.getBlock("latest");
const blockTimestamp = latestBlock!.timestamp;
const newAadhaarData = prepareAadhaarRegisterTestData( const newAadhaarData = prepareAadhaarRegisterTestData(
privateKeyPem, privateKeyPem,
pubkeyPem, pubkeyPem,
@@ -183,8 +186,7 @@ describe("Aadhaar Registration test", function () {
"M", "M",
"110051", "110051",
"WB", "WB",
//timestamp 30 minutes ago and converted to timestamp string (blockTimestamp - 30 * 60).toString(),
new Date(Date.now() - 30 * 60 * 1000).getTime().toString(),
); );
const newRegisterProof = await generateRegisterAadhaarProof(registerSecret, newAadhaarData.inputs); const newRegisterProof = await generateRegisterAadhaarProof(registerSecret, newAadhaarData.inputs);

View File

@@ -37,7 +37,6 @@
"@babel/core": "^7.28.4", "@babel/core": "^7.28.4",
"@babel/runtime": "^7.28.4", "@babel/runtime": "^7.28.4",
"@noble/curves": "1.9.7", "@noble/curves": "1.9.7",
"@noble/hashes": "1.8.0",
"@swc/core": "1.7.36", "@swc/core": "1.7.36",
"@tamagui/animations-react-native": "1.126.14", "@tamagui/animations-react-native": "1.126.14",
"@tamagui/toast": "1.126.14", "@tamagui/toast": "1.126.14",

View File

@@ -151,7 +151,7 @@
"dependencies": { "dependencies": {
"@babel/runtime": "^7.28.3", "@babel/runtime": "^7.28.3",
"@selfxyz/common": "workspace:^", "@selfxyz/common": "workspace:^",
"@selfxyz/euclid": "^0.4.1", "@selfxyz/euclid": "^0.6.0",
"@xstate/react": "^5.0.5", "@xstate/react": "^5.0.5",
"node-forge": "^1.3.1", "node-forge": "^1.3.1",
"react-native-nfc-manager": "^3.17.1", "react-native-nfc-manager": "^3.17.1",

View File

@@ -18,6 +18,7 @@ const BRANCH = 'main';
// Environment detection // Environment detection
const isCI = process.env.CI === 'true'; const isCI = process.env.CI === 'true';
const repoToken = process.env.SELFXYZ_INTERNAL_REPO_PAT; 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'; const isDryRun = process.env.DRY_RUN === 'true';
function log(message, type = 'info') { function log(message, type = 'info') {
@@ -89,19 +90,24 @@ function setupSubmodule() {
let submoduleUrl; 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 // CI environment with Personal Access Token
log('CI detected: Using SELFXYZ_INTERNAL_REPO_PAT for submodule', 'info'); // Security: NEVER embed credentials in git URLs. Rely on CI-provided auth via:
submoduleUrl = `https://${repoToken}@github.com/${GITHUB_ORG}/${REPO_NAME}.git`; // - ~/.netrc, a Git credential helper, or SSH agent configuration.
submoduleUrl = `https://github.com/${GITHUB_ORG}/${REPO_NAME}.git`;
} else if (isCI) { } 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'); log('This is expected for forked PRs or environments without access to private modules', 'info');
return false; // Return false to indicate setup was skipped return false; // Return false to indicate setup was skipped
} else if (usingHTTPSGitAuth()) { } else if (usingHTTPSGitAuth()) {
submoduleUrl = `https://github.com/${GITHUB_ORG}/${REPO_NAME}.git`; submoduleUrl = `https://github.com/${GITHUB_ORG}/${REPO_NAME}.git`;
} else { } else {
// Local development with SSH // Local development with SSH
log('Local development: Using SSH for submodule', 'info');
submoduleUrl = `git@github.com:${GITHUB_ORG}/${REPO_NAME}.git`; submoduleUrl = `git@github.com:${GITHUB_ORG}/${REPO_NAME}.git`;
} }
@@ -113,7 +119,7 @@ function setupSubmodule() {
} else { } else {
// Add submodule // Add submodule
const addCommand = `git submodule add -b ${BRANCH} "${submoduleUrl}" mobile-sdk-native`; 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 // Security: Run command silently to avoid token exposure in logs
runCommand(addCommand, { stdio: 'pipe' }); runCommand(addCommand, { stdio: 'pipe' });
} else { } else {
@@ -125,7 +131,7 @@ function setupSubmodule() {
return true; // Return true to indicate successful setup return true; // Return true to indicate successful setup
} catch (error) { } catch (error) {
if (isCI) { 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 { } else {
log('Submodule setup failed. Ensure you have SSH access to the repository.', 'error'); 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 // Security: Remove credential-embedded remote URL after setup
if (isCI && repoToken && !isDryRun) { if (isCI && (appToken || repoToken) && !isDryRun) {
scrubGitRemoteUrl(); scrubGitRemoteUrl();
} }

View File

@@ -79,6 +79,7 @@ export const BackupEvents = {
}; };
export const DocumentEvents = { export const DocumentEvents = {
COUNTRY_HELP_TAPPED: 'Document: Country Help Tapped',
ADD_NEW_AADHAAR_SELECTED: 'Document: Add Aadhaar', ADD_NEW_AADHAAR_SELECTED: 'Document: Add Aadhaar',
ADD_NEW_MOCK_SELECTED: 'Document: Add New Document via Mock', ADD_NEW_MOCK_SELECTED: 'Document: Add New Document via Mock',
ADD_NEW_SCAN_SELECTED: 'Document: Add New Document via Scan', ADD_NEW_SCAN_SELECTED: 'Document: Add New Document via Scan',

View File

@@ -5,15 +5,18 @@
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import { commonNames } from '@selfxyz/common/constants/countries'; 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 { RoundFlag } from '../../components';
import { DocumentEvents } from '../../constants/analytics';
import { useSelfClient } from '../../context'; import { useSelfClient } from '../../context';
import { useCountries } from '../../documents/useCountries'; import { useCountries } from '../../documents/useCountries';
import { buttonTap } from '../../haptic'; import { buttonTap } from '../../haptic';
import { SdkEvents } from '../../types/events'; import { SdkEvents } from '../../types/events';
const CountryPickerScreen: React.FC = () => { const CountryPickerScreen: React.FC<SafeArea> & { statusBar: typeof CountryPickerUI.statusBar } = ({
insets,
}: SafeArea) => {
const selfClient = useSelfClient(); const selfClient = useSelfClient();
const [searchValue, setSearchValue] = useState(''); const [searchValue, setSearchValue] = useState('');
@@ -57,9 +60,9 @@ const CountryPickerScreen: React.FC = () => {
const onSearchChange = useCallback((value: string) => { const onSearchChange = useCallback((value: string) => {
setSearchValue(value); setSearchValue(value);
}, []); }, []);
return ( return (
<CountryPickerUI <CountryPickerUI
insets={insets}
isLoading={loading} isLoading={loading}
countries={countryList} countries={countryList}
onCountrySelect={onCountrySelect} onCountrySelect={onCountrySelect}
@@ -69,11 +72,11 @@ const CountryPickerScreen: React.FC = () => {
getCountryName={getCountryName} getCountryName={getCountryName}
searchValue={searchValue} searchValue={searchValue}
onClose={selfClient.goBack} onClose={selfClient.goBack}
onInfoPress={() => console.log('Info pressed TODO: Implement')} onInfoPress={() => selfClient.trackEvent(DocumentEvents.COUNTRY_HELP_TAPPED)}
onSearchChange={onSearchChange} onSearchChange={onSearchChange}
/> />
); );
}; };
CountryPickerScreen.displayName = 'CountryPickerScreen'; CountryPickerScreen.displayName = 'CountryPickerScreen';
CountryPickerScreen.statusBar = CountryPickerUI.statusBar;
export default CountryPickerScreen; export default CountryPickerScreen;

View File

@@ -46,12 +46,15 @@ target "SelfDemoApp" do
nfc_repo_url = if !is_selfxyz_repo nfc_repo_url = if !is_selfxyz_repo
puts "📦 Using public NFCPassportReader for external fork (#{ENV["GITHUB_REPOSITORY"]})" puts "📦 Using public NFCPassportReader for external fork (#{ENV["GITHUB_REPOSITORY"]})"
"https://github.com/PLACEHOLDER/NFCPassportReader.git" "https://github.com/PLACEHOLDER/NFCPassportReader.git"
elsif ENV["GITHUB_ACTIONS"] == "true" && ENV["SELFXYZ_INTERNAL_REPO_PAT"] && !ENV["SELFXYZ_INTERNAL_REPO_PAT"].empty? elsif ENV["GITHUB_ACTIONS"] == "true"
puts "📦 Using private NFCPassportReader with PAT (selfxyz GitHub Actions)" # CI: NEVER embed credentials in URLs. Rely on workflow-provided auth via:
"https://#{ENV["SELFXYZ_INTERNAL_REPO_PAT"]}@github.com/selfxyz/NFCPassportReader.git" # - ~/.netrc or a Git credential helper, and token masking in logs.
"https://github.com/selfxyz/NFCPassportReader.git"
elsif using_https_git_auth? elsif using_https_git_auth?
# Local development with HTTPS GitHub auth via gh - use HTTPS to private repo
"https://github.com/selfxyz/NFCPassportReader.git" "https://github.com/selfxyz/NFCPassportReader.git"
else else
# Local development in selfxyz repo - use SSH to private repo
puts "📦 Using SSH for private NFCPassportReader (local selfxyz development)" puts "📦 Using SSH for private NFCPassportReader (local selfxyz development)"
"git@github.com:selfxyz/NFCPassportReader.git" "git@github.com:selfxyz/NFCPassportReader.git"
end end

View File

@@ -5,7 +5,7 @@ import ScreenLayout from '../components/ScreenLayout';
export default function CountrySelection({ onBack }: { onBack: () => void }) { export default function CountrySelection({ onBack }: { onBack: () => void }) {
return ( return (
<ScreenLayout title="GETTING STARTED" onBack={onBack}> <ScreenLayout title="GETTING STARTED" onBack={onBack}>
<SDKCountryPickerScreen /> <SDKCountryPickerScreen insets={{ top: 0, bottom: 0 }} />
</ScreenLayout> </ScreenLayout>
); );
} }

1532
yarn.lock

File diff suppressed because it is too large Load Diff