feat(presets): KYC entry-requirement validators (badge + registry) - #27
feat(presets): KYC entry-requirement validators (badge + registry)#27starknetdev wants to merge 1 commit into
Conversation
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 encountered an error —— View job I'll analyze this and get back to you. |
Codex Review - General Engineering ReviewReview process failed to complete. |
|
Claude encountered an error —— View job I'll analyze this and get back to you. |
Codex Review - Cairo/Starknet Contract ReviewReview process failed to complete. |
There was a problem hiding this comment.
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.
| 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)); |
There was a problem hiding this comment.
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));
| 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)); |
There was a problem hiding this comment.
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 Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
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 onEntryRequirementExtensionComponent, are per-account and quota-aware, and consume only a boolean signal (no attributes pulled into the tournament layer).kyc_badge_validatorKycBadgeviahas_badge(account)kyc_registry_validatorget_propsregistered + over_18kyc_badge_validatoris 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_presetssuite is green (scarb fmtclean). Registry reads are mocked viastart_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
KycBadgecontract and the backend that mints it after verifying a zkpassport proof. Thekyc_registry_validatoris for teams that prefer a self-owned, single-country trust root over the third-party zkpassport stack.🤖 Generated with Claude Code