Skip to content

feat(presets): KYC entry-requirement validators (badge + registry) - #27

Open
starknetdev wants to merge 1 commit into
mainfrom
feat/kyc-entry-validators
Open

feat(presets): KYC entry-requirement validators (badge + registry)#27
starknetdev wants to merge 1 commit into
mainfrom
feat/kyc-entry-validators

Conversation

@starknetdev

Copy link
Copy Markdown
Member

What

Two reusable entry-requirement validators that gate tournament/campaign entry on passport-proven KYC, sitting next to the existing zkpassport_validator. Both are built on EntryRequirementExtensionComponent, are per-account and quota-aware, and consume only a boolean signal (no attributes pulled into the tournament layer).

Validator Gates on Trust root
kyc_badge_validator a dedicated soulbound KycBadge via has_badge(account) the off-chain zkpassport → badge flow (verification-app) — proof verified server-side, badge minted
kyc_registry_validator the zk-kyc-demo on-chain attribute registry via get_props passport-proven registered + over_18

kyc_badge_validator is the on-chain consumer of verification-app#100. Because the badge is revocable, a withdrawn KYC status is reflected on-chain and is bannable when the context is configured that way.

Config

  • kyc_badge_validator: config = [badge_address, bannable?]
  • kyc_registry_validator: config = [registry_address, bannable?]

A tournament picks one by setting EntryRequirementType::extension({ address, config }). They're interchangeable at the Budokan entry hook, so an organizer chooses the trust root per tournament.

Tests

15 new snforge tests pass (8 badge + 7 registry); the full metagame_extensions_presets suite is green (scarb fmt clean). Registry reads are mocked via start_mock_call, so no deployed contracts or fork access are needed.

Notes

This is the privacy-leaning counterpart to a registry: see verification-app#100 for the dedicated KycBadge contract and the backend that mints it after verifying a zkpassport proof. The kyc_registry_validator is for teams that prefer a self-owned, single-country trust root over the third-party zkpassport stack.

🤖 Generated with Claude Code

Two reusable entry validators that gate tournament entry on passport-proven KYC,
sitting alongside the existing zkpassport_validator:

- kyc_badge_validator — gates on a dedicated soulbound KycBadge via has_badge().
  The on-chain consumer of the off-chain zkpassport → badge flow (verification-app).
  Boolean credential only; revocable badge ⇒ bannable when configured.
- kyc_registry_validator — gates on the zk-kyc-demo on-chain attribute registry
  (get_props), admitting registered + over-18.

Both built on EntryRequirementExtensionComponent; per-account, quota-aware. 15
snforge tests pass; full presets suite green. README updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@claude

claude Bot commented Jun 29, 2026

Copy link
Copy Markdown

Claude encountered an error —— View job


I'll analyze this and get back to you.

@github-actions

Copy link
Copy Markdown

Codex Review - General Engineering Review

Review process failed to complete.

@claude

claude Bot commented Jun 29, 2026

Copy link
Copy Markdown

Claude encountered an error —— View job


I'll analyze this and get back to you.

@github-actions

Copy link
Copy Markdown

Codex Review - Cairo/Starknet Contract Review

Review process failed to complete.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces two new entry requirement validators, KycBadgeValidator and KycRegistryValidator, along with their respective unit tests and documentation. The KycBadgeValidator gates tournament entry based on a soulbound KYC membership badge, while the KycRegistryValidator relies on a passport-proven zk-KYC registration registry. The feedback highlights a discrepancy in both validators where entries_left does not reject non-empty qualifications, unlike validate_entry, which could lead to inconsistent behavior.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +142 to +149
fn entries_left(
self: @ContractState,
context_owner: ContractAddress,
context_id: u64,
player_address: ContractAddress,
qualification: Span<felt252>,
) -> Option<u32> {
let badge_addr = self.context_badge_address.read((context_owner, context_id));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In validate_entry, any non-empty qualification is rejected immediately. However, entries_left does not perform this check, which can lead to a discrepancy where a client queries entries_left with a non-empty qualification and receives a positive number of entries, but the actual entry registration is rejected. We should return Option::Some(0) if qualification is not empty to align with validate_entry's rejection semantics.

        fn entries_left(
            self: @ContractState,
            context_owner: ContractAddress,
            context_id: u64,
            player_address: ContractAddress,
            qualification: Span<felt252>,
        ) -> Option<u32> {
            if qualification.len() != 0 {
                return Option::Some(0);
            }

            let badge_addr = self.context_badge_address.read((context_owner, context_id));

Comment on lines +168 to +176
fn entries_left(
self: @ContractState,
context_owner: ContractAddress,
context_id: u64,
player_address: ContractAddress,
qualification: Span<felt252>,
) -> Option<u32> {
// Not eligible -> no entries, matching validate_entry's rejection semantics.
let registry_addr = self.context_registry_address.read((context_owner, context_id));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In validate_entry, any non-empty qualification is rejected immediately. However, entries_left does not perform this check, which can lead to a discrepancy where a client queries entries_left with a non-empty qualification and receives a positive number of entries, but the actual entry registration is rejected. We should return Option::Some(0) if qualification is not empty to align with validate_entry's rejection semantics.

        fn entries_left(
            self: @ContractState,
            context_owner: ContractAddress,
            context_id: u64,
            player_address: ContractAddress,
            qualification: Span<felt252>,
        ) -> Option<u32> {
            if qualification.len() != 0 {
                return Option::Some(0);
            }

            // Not eligible -> no entries, matching validate_entry's rejection semantics.
            let registry_addr = self.context_registry_address.read((context_owner, context_id));

@codecov

codecov Bot commented Jun 29, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 79.16667% with 20 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...src/entry_requirement/kyc_registry_validator.cairo 78.43% 11 Missing ⚠️
...ts/src/entry_requirement/kyc_badge_validator.cairo 80.00% 9 Missing ⚠️

📢 Thoughts on this report? Let us know!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant