Skip to content

rev-share-limit Referral Program Disqualification#1712

Merged
Goader merged 7 commits intomainfrom
feat/referral-program-disqualification
Mar 4, 2026
Merged

rev-share-limit Referral Program Disqualification#1712
Goader merged 7 commits intomainfrom
feat/referral-program-disqualification

Conversation

@Goader
Copy link
Contributor

@Goader Goader commented Mar 3, 2026

rev-share-limit Referral Program Disqualification

Summary

  • Added disqualifications field to ReferralProgramAdminDisqualification
  • isReferrerQualifiedRevShareLimit now also checks for disqualification
  • isReferrerQualifiedRevShareLimit is now used during the leaderboard construction, never letting a disqualified referrer get "qualified", and therefore preventing from claiming anything from the pool
  • Appropriate validation, schemas, builders updates, and tests

Why

  • We may need to disqualify some referrers during the next Referral Program Edition, this allows to do that with a simple custom JSON config and ENSApi restart

Testing

  • Automatic, CI testing
  • Introduced some new tests for new logic
  • Manual validation

Notes for Reviewer (Optional)

  • disqualifications field
  • How it is integrated into leaderboard building

Pre-Review Checklist (Blocking)

  • This PR does not introduce significant changes and is low-risk to review quickly.
  • Relevant changesets are included (or are not required)

@Goader Goader self-assigned this Mar 3, 2026
Copilot AI review requested due to automatic review settings March 3, 2026 22:58
@changeset-bot
Copy link

changeset-bot bot commented Mar 3, 2026

🦋 Changeset detected

Latest commit: cab9724

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 18 packages
Name Type
@namehash/ens-referrals Major
ensapi Major
ensindexer Major
ensadmin Major
ensrainbow Major
fallback-ensapi Major
@ensnode/datasources Major
@ensnode/ensrainbow-sdk Major
@ensnode/ensnode-schema Major
@ensnode/ensnode-react Major
@ensnode/ensnode-sdk Major
@ensnode/ponder-sdk Major
@ensnode/ponder-subgraph Major
@ensnode/shared-configs Major
@docs/ensnode Major
@docs/ensrainbow Major
@docs/mintlify Major
@namehash/namehash-ui Major

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel
Copy link
Contributor

vercel bot commented Mar 3, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

3 Skipped Deployments
Project Deployment Actions Updated (UTC)
admin.ensnode.io Skipped Skipped Mar 4, 2026 0:12am
ensnode.io Skipped Skipped Mar 4, 2026 0:12am
ensrainbow.io Skipped Skipped Mar 4, 2026 0:12am

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 3, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds admin-disqualification support to the rev-share-limit referral model: new disqualification type and rules, rule validation and builder changes, metrics and schema/serializer extensions to propagate disqualification state and reason, leaderboard logic/tests updated to use qualification function that considers disqualifications.

Changes

Cohort / File(s) Summary
Changeset
.changeset/proud-wolves-cheer.md
Add changeset entry for a minor version bump documenting admin-disqualification support.
Core Rules & Types
packages/ens-referrals/src/v1/award-models/rev-share-limit/rules.ts
Add ReferralProgramEditionDisqualification interface; add disqualifications to rules; validate entries and duplicates; extend buildReferralProgramRulesRevShareLimit to accept disqualifications; change isReferrerQualifiedRevShareLimit signature to accept referrer and check disqualifications.
Schemas & Serialization
packages/ens-referrals/src/v1/award-models/rev-share-limit/api/zod-schemas.ts, packages/ens-referrals/src/v1/award-models/rev-share-limit/api/serialize.ts
Add admin-disqualification zod schema; extend rules, awarded, and unranked schemas with isAdminDisqualified and adminDisqualificationReason and refinements; serializers now include disqualification fields.
Metrics, Builders & Validators
packages/ens-referrals/src/v1/award-models/rev-share-limit/metrics.ts
Expose isAdminDisqualified and adminDisqualificationReason on ranked/unranked metrics; builders/validators derive and enforce disqualification state and reason; enforce zero awardPool when admin-disqualified; update function signatures to accept rules where required.
Leaderboard & Tests
packages/ens-referrals/src/v1/award-models/rev-share-limit/leaderboard.ts, packages/ens-referrals/src/v1/award-models/rev-share-limit/leaderboard.test.ts
Use isReferrerQualifiedRevShareLimit(referrer, ...) for qualification checks; add comprehensive tests covering admin disqualification scenarios, duplicates, and edge cases; test helpers accept disqualifications.
Edition Integration & Metrics Call Sites
packages/ens-referrals/src/v1/edition-defaults.ts, packages/ens-referrals/src/v1/edition-metrics.ts
Pass empty disqualifications array in defaults call; supply rules to buildUnrankedReferrerMetricsRevShareLimit where required.

Sequence Diagram(s)

sequenceDiagram
  participant Client as Client
  participant Leaderboard as Leaderboard
  participant Rules as Rules
  participant Metrics as MetricsBuilder
  participant Serializer as Serializer

  Client->>Leaderboard: request leaderboard computation
  Leaderboard->>Rules: fetch rules (includes `disqualifications`)
  Leaderboard->>Metrics: build metrics(referrer, rules)
  Metrics->>Rules: isReferrerQualifiedRevShareLimit(referrer, revenue, rules)
  Metrics-->>Leaderboard: return metrics (isAdminDisqualified, reason, isQualified)
  Leaderboard->>Serializer: serialize results (include admin disqualification fields)
  Serializer-->>Client: respond with serialized leaderboard
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

ensnode-internal

Poem

🐇
I hopped through rules and marked the list with care,
A carrot for the honest, a note for one unfair.
When admins sigh and write a reason short and sweet,
The scoreboard skips a pawprint and the tally loses treat. 🥕

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: adding disqualification support to the rev-share-limit referral program model.
Description check ✅ Passed The description covers all required template sections with appropriate detail: Summary, Why, Testing, Notes, and completed Pre-Review Checklist.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/referral-program-disqualification

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds admin-controlled “disqualification” support to the rev-share-limit referral program model so specific referrers can be excluded from qualification/awards and surfaced in metrics + API output.

Changes:

  • Extend rev-share-limit rules with a disqualifications[] list (referrer + reason) and incorporate it into qualification logic.
  • Add disqualification fields to ranked/unranked referrer metrics (flags + reason) and serialize/validate them via zod schemas.
  • Update leaderboard qualification check and edition metrics construction; add tests for disqualification behavior.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
packages/ens-referrals/src/v1/edition-metrics.ts Passes rules into unranked rev-share-limit metrics builder to compute disqualification flags/reason.
packages/ens-referrals/src/v1/edition-defaults.ts Supplies an explicit empty disqualification list for the default rev-share-limit edition.
packages/ens-referrals/src/v1/award-models/rev-share-limit/rules.ts Introduces disqualification types/list, validates uniqueness, and updates qualification logic to exclude disqualified referrers.
packages/ens-referrals/src/v1/award-models/rev-share-limit/metrics.ts Adds admin-disqualification fields to referrer metrics and validates/derives them from rules.
packages/ens-referrals/src/v1/award-models/rev-share-limit/leaderboard.ts Uses shared qualification helper (now includes disqualification) during event processing.
packages/ens-referrals/src/v1/award-models/rev-share-limit/leaderboard.test.ts Adds coverage for disqualification scenarios and duplicate-disqualification validation.
packages/ens-referrals/src/v1/award-models/rev-share-limit/api/zod-schemas.ts Extends API schemas for rules + metrics to include disqualification structures/fields.
packages/ens-referrals/src/v1/award-models/rev-share-limit/api/serialize.ts Serializes disqualifications and new metric fields into API-friendly shapes.
.changeset/proud-wolves-cheer.md Declares a minor version bump for the new disqualification feature.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@packages/ens-referrals/src/v1/award-models/rev-share-limit/api/zod-schemas.ts`:
- Around line 48-52: Add a Zod-level uniqueness check to the disqualifications
array schema: update the schema built with
makeReferralProgramAdminDisqualificationSchema (the disqualifications:
z.array(...).default([]) definition) to include a .refine(...) that ensures no
duplicate referrer addresses (or the specific address field used in each
disqualification object) exist in the array, returning a clear error
path/message; this mirrors the runtime duplicate check in
validateReferralProgramRulesRevShareLimit and provides earlier validation
feedback for API inputs.

In `@packages/ens-referrals/src/v1/award-models/rev-share-limit/metrics.ts`:
- Around line 338-339: The lookup for unranked disqualification currently uses
the raw input `referrer`; change it to use the normalized address
`metrics.referrer` instead so mixed-case equivalents match validation.
Specifically, update the `rules.disqualifications.find((d) => d.referrer ===
referrer)` calls (including the occurrences that set `disqualification` and
`isAdminDisqualified`) to compare against `metrics.referrer` (or otherwise
normalizedReferrer) so all checks use the same normalized identifier.

In `@packages/ens-referrals/src/v1/award-models/rev-share-limit/rules.ts`:
- Around line 99-105: The duplicate-address check in rules.ts currently builds
disqualificationAddresses from rules.disqualifications.map(d => d.referrer) and
then compares via uniqueDisqualificationAddresses (a Set), which misses
duplicates that differ only by case; normalize each referrer (e.g., to lowercase
or use a canonical checksum via a utility like ethers.utils.getAddress) when
creating disqualificationAddresses before creating
uniqueDisqualificationAddresses so that addresses with different casing are
treated as identical and the duplicate error is thrown correctly.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a0be9a6 and edf55a2.

📒 Files selected for processing (9)
  • .changeset/proud-wolves-cheer.md
  • packages/ens-referrals/src/v1/award-models/rev-share-limit/api/serialize.ts
  • packages/ens-referrals/src/v1/award-models/rev-share-limit/api/zod-schemas.ts
  • packages/ens-referrals/src/v1/award-models/rev-share-limit/leaderboard.test.ts
  • packages/ens-referrals/src/v1/award-models/rev-share-limit/leaderboard.ts
  • packages/ens-referrals/src/v1/award-models/rev-share-limit/metrics.ts
  • packages/ens-referrals/src/v1/award-models/rev-share-limit/rules.ts
  • packages/ens-referrals/src/v1/edition-defaults.ts
  • packages/ens-referrals/src/v1/edition-metrics.ts

@vercel vercel bot temporarily deployed to Preview – admin.ensnode.io March 4, 2026 00:04 Inactive
@vercel vercel bot temporarily deployed to Preview – ensrainbow.io March 4, 2026 00:04 Inactive
@vercel vercel bot temporarily deployed to Preview – ensnode.io March 4, 2026 00:04 Inactive
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@packages/ens-referrals/src/v1/award-models/rev-share-limit/api/zod-schemas.ts`:
- Around line 29-32: The schema for the "reason" field currently uses
z.string().min(1) which lets whitespace-only values pass but later fails in
rules.ts when checking d.reason.trim().length; update the schema in
zod-schemas.ts (the z.object that uses makeLowercaseAddressSchema and
valueLabel) to perform trimming/empty-check at the schema level — e.g. apply a
.trim() or .transform(s => s.trim()) and then enforce .min(1) or .refine(s =>
s.length > 0) with the same `${valueLabel}.reason must not be empty` message so
whitespace-only strings are rejected consistently before hitting rules.ts.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between edf55a2 and f25ef17.

📒 Files selected for processing (3)
  • packages/ens-referrals/src/v1/award-models/rev-share-limit/api/zod-schemas.ts
  • packages/ens-referrals/src/v1/award-models/rev-share-limit/metrics.ts
  • packages/ens-referrals/src/v1/award-models/rev-share-limit/rules.ts

Copilot AI review requested due to automatic review settings March 4, 2026 00:20
@vercel vercel bot temporarily deployed to Preview – ensrainbow.io March 4, 2026 00:20 Inactive
@vercel vercel bot temporarily deployed to Preview – ensnode.io March 4, 2026 00:20 Inactive
@vercel vercel bot temporarily deployed to Preview – admin.ensnode.io March 4, 2026 00:20 Inactive
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@packages/ens-referrals/src/v1/award-models/rev-share-limit/api/zod-schemas.ts`:
- Around line 84-85: The schema currently allows empty or whitespace strings for
adminDisqualificationReason; update the zod schema for
adminDisqualificationReason to require a non-empty trimmed string when present
(mirror the validation used for the reason field) and keep it nullable only for
the null case; ensure the conditional invariant tied to isAdminDisqualified is
enforced (use the same z.string().min(1).transform or .refine pattern used by
reason and/or a .superRefine that checks when isAdminDisqualified === true then
adminDisqualificationReason is a non-empty string) so empty/whitespace values
are rejected.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f25ef17 and fe0ff8a.

📒 Files selected for processing (1)
  • packages/ens-referrals/src/v1/award-models/rev-share-limit/api/zod-schemas.ts

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@vercel vercel bot temporarily deployed to Preview – admin.ensnode.io March 4, 2026 01:14 Inactive
@vercel vercel bot temporarily deployed to Preview – ensnode.io March 4, 2026 01:14 Inactive
@vercel vercel bot temporarily deployed to Preview – ensrainbow.io March 4, 2026 01:14 Inactive
Copilot AI review requested due to automatic review settings March 4, 2026 01:19
@vercel vercel bot temporarily deployed to Preview – ensrainbow.io March 4, 2026 01:19 Inactive
@vercel vercel bot temporarily deployed to Preview – admin.ensnode.io March 4, 2026 01:19 Inactive
@vercel vercel bot temporarily deployed to Preview – ensnode.io March 4, 2026 01:19 Inactive
@Goader Goader marked this pull request as ready for review March 4, 2026 01:23
@Goader Goader requested a review from a team as a code owner March 4, 2026 01:23
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

packages/ens-referrals/src/v1/award-models/rev-share-limit/metrics.ts:76

  • JSDoc for RankedReferrerMetricsRevShareLimit.isQualified references awardPoolShare and implies a non-zero award is guaranteed. In rev-share-limit there is no awardPoolShare field, and isQualified can still be true while awardPoolApproxValue.amount is 0 (e.g., pool depleted). Update the comment to reference the correct field(s) and describe isQualified as eligibility to claim from the pool (not a guarantee of non-zero payout).
  /**
   * Identifies if the referrer meets the qualifications of the {@link ReferralProgramRulesRevShareLimit} to receive a non-zero `awardPoolShare`.
   *
   * @invariant true if and only if `totalBaseRevenueContribution` is greater than or equal to
   *   {@link ReferralProgramRulesRevShareLimit.minQualifiedRevenueContribution} AND

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 4, 2026

Greptile Summary

This PR introduces admin disqualification support for the rev-share-limit referral program edition, allowing operators to exclude specific referrer addresses (with a reason) from receiving any award pool allocation by configuring a disqualifications list in the edition rules JSON.

Key changes:

  • rules.ts: Adds ReferralProgramAdminDisqualification interface and disqualifications: ReferralProgramAdminDisqualification[] field to ReferralProgramRulesRevShareLimit. Validates each entry for lowercase address format, non-empty reason, and uniqueness. Updates isReferrerQualifiedRevShareLimit to gate on both the revenue threshold AND the absence of an admin disqualification.
  • leaderboard.ts: The hot-path qualification check now uses isReferrerQualifiedRevShareLimit, which means a disqualified referrer can never set wasQualified = true, never claims from the pool, and the pool balance is correctly preserved for subsequent qualified referrers.
  • metrics.ts: Adds isAdminDisqualified and adminDisqualificationReason fields to RankedReferrerMetricsRevShareLimit (and by extension AwardedReferrerMetricsRevShareLimit and UnrankedReferrerMetricsRevShareLimit), with full build-time and validation-time consistency checks.
  • api/zod-schemas.ts and api/serialize.ts: Extend API schemas and serializers to expose the new fields.
  • leaderboard.test.ts: Six new test cases covering the disqualification scenarios thoroughly.

Confidence Score: 4/5

  • This PR is safe to merge; the core disqualification logic is correct and well-tested, with only minor style inconsistencies.
  • The disqualification logic is correctly threaded through the leaderboard race algorithm (pool is never consumed by a disqualified referrer), the validators enforce all stated invariants, and the test suite covers the main edge cases. The score is 4 rather than 5 due to an inconsistency in address normalization: isReferrerQualifiedRevShareLimit in rules.ts explicitly normalizes via normalizeAddress, while the equivalent disqualification lookups in metrics.ts rely implicitly on the lowercase-address invariant without normalization. Additionally, buildRankedReferrerMetricsRevShareLimit performs a redundant O(n) disqualification scan alongside the one already inside isReferrerQualifiedRevShareLimit. Neither issue causes a bug today, but both add fragility and minor inefficiency.
  • packages/ens-referrals/src/v1/award-models/rev-share-limit/metrics.ts warrants a second look for the normalization inconsistency and redundant disqualification lookups.

Important Files Changed

Filename Overview
packages/ens-referrals/src/v1/award-models/rev-share-limit/leaderboard.ts Correctly delegates qualification check to isReferrerQualifiedRevShareLimit, which now gates both the revenue threshold and admin disqualification. Disqualified referrers can never set wasQualified = true, so they never claim from the pool and the pool balance is preserved for later qualified referrers.
packages/ens-referrals/src/v1/award-models/rev-share-limit/rules.ts Adds the ReferralProgramAdminDisqualification interface and disqualifications field to ReferralProgramRulesRevShareLimit. Validation enforces lowercase addresses, non-empty reasons, and uniqueness. isReferrerQualifiedRevShareLimit is extended to check both the revenue threshold and the disqualification list.
packages/ens-referrals/src/v1/award-models/rev-share-limit/metrics.ts Adds isAdminDisqualified and adminDisqualificationReason to ranked/unranked metrics and validates their consistency. Two minor style concerns: disqualification lookups don't normalize addresses (unlike isReferrerQualifiedRevShareLimit), and buildRankedReferrerMetricsRevShareLimit performs a redundant O(n) disqualification scan alongside the one inside isReferrerQualifiedRevShareLimit.
packages/ens-referrals/src/v1/award-models/rev-share-limit/api/zod-schemas.ts Adds Zod schemas for ReferralProgramAdminDisqualification and the new disqualifications field with a duplicate-address refinement. Also adds isAdminDisqualified/adminDisqualificationReason fields to awarded/unranked metrics schemas with appropriate cross-field refinements.
packages/ens-referrals/src/v1/award-models/rev-share-limit/leaderboard.test.ts Comprehensive new test suite covering: no disqualifications (baseline), disqualified referrer who met the threshold (pool preserved), disqualified referrer below threshold, ranking interactions between disqualified/qualified/unqualified, multiple disqualifications, and duplicate-address rejection.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[ReferralEvent stream] --> B[Sort by checkpoint ID]
    B --> C{For each event}
    C --> D[Update referrer state\ntotalReferrals / duration / revenue]
    D --> E[Compute totalBaseRevenueAmount]
    E --> F[isReferrerQualifiedRevShareLimit]
    F --> G{Revenue threshold\nmet?}
    G -- No --> J[No pool claim\nwasQualified stays false]
    G -- Yes --> H{Admin\ndisqualified?}
    H -- Yes --> J
    H -- No --> I{wasQualified\nalready?}
    I -- No --> K[Claim accumulated award\nfrom pool, set wasQualified=true]
    I -- Yes --> L[Claim incremental award\nfrom pool]
    J --> C
    K --> C
    L --> C
    C --> M[Sort referrers: pool claim desc,\nduration desc, address desc]
    M --> N[buildRankedReferrerMetricsRevShareLimit\nisAdminDisqualified / adminDisqualificationReason]
    N --> O[buildAwardedReferrerMetricsRevShareLimit\nawardPoolApproxValue validated = 0 if disqualified]
    O --> P[ReferrerLeaderboardRevShareLimit]
Loading

Last reviewed commit: 77530ea

Comment on lines +138 to +139
rules.disqualifications.find((d) => d.referrer === referrer.referrer) ?? null;

Copy link
Contributor

Choose a reason for hiding this comment

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

Inconsistent address normalization vs. isReferrerQualifiedRevShareLimit

In rules.ts, isReferrerQualifiedRevShareLimit explicitly normalizes the referrer before comparing against disqualification entries:

const normalizedReferrer = normalizeAddress(referrer);
const isAdminDisqualified = rules.disqualifications.some(
  (d) => d.referrer === normalizedReferrer,
);

But the direct disqualification lookups throughout metrics.ts — in buildRankedReferrerMetricsRevShareLimit (line 138), validateRankedReferrerMetricsRevShareLimit (line 116), validateUnrankedReferrerMetricsRevShareLimit (line 266), and buildUnrankedReferrerMetricsRevShareLimit (line 339) — skip normalization and rely on the invariant that metrics.referrer is always lowercase.

This is currently safe because buildReferrerMetrics normalizes via normalizeAddress and validateReferrerMetrics enforces lowercase, so the invariant holds. However, the inconsistency is a potential footgun: if a future code path calls these builders with an unnormalized address (or if the address invariant is ever relaxed), the disqualification lookup would silently return null for a disqualified referrer, incorrectly leaving isAdminDisqualified = false.

Consider normalizing consistently in metrics.ts as well, or adding an explicit comment that normalization is already guaranteed by the ReferrerMetrics invariant.

Copy link
Member

Choose a reason for hiding this comment

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

We should guarantee we store values in normalized form so that while processing those values we don't need to normalize again or check for normalization.

Copy link
Contributor

Choose a reason for hiding this comment

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

Agreed. The current code already follows this pattern - addresses are normalized at creation in buildReferrerMetrics and validated to be lowercase in validateReferrerMetrics, so all downstream processing can safely assume normalized addresses.

The inconsistency I flagged (isReferrerQualifiedRevShareLimit explicitly normalizing while metrics.ts doesn't) actually makes sense now: isReferrerQualifiedRevShareLimit is a public entry point that might receive raw input, while metrics.ts functions operate on already-validated metrics.

For maintainability, consider adding a brief comment at the top of metrics.ts stating the address normalization invariant, like:

Suggested change
rules.disqualifications.find((d) => d.referrer === referrer.referrer) ?? null;
// All address comparisons in this module assume metrics.referrer is already normalized (lowercase).
// Normalization is enforced by buildReferrerMetrics and validateReferrerMetrics.

This documents the design decision and prevents future confusion.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 4, 2026

Additional Comments (1)

packages/ens-referrals/src/v1/award-models/rev-share-limit/metrics.ts
Redundant disqualification check in builder

buildRankedReferrerMetricsRevShareLimit performs its own rules.disqualifications.find(...) lookup (line 138) to set isAdminDisqualified, and then immediately calls isReferrerQualifiedRevShareLimit (lines 143–147), which runs a second — nearly identical — disqualification scan internally. Every referrer therefore triggers two O(n) linear scans of rules.disqualifications.

For the expected small sizes of the disqualification list this is harmless, but extracting the disqualification lookup into isReferrerQualifiedRevShareLimit's return value (or a separate helper), or pre-computing a Set<Address> of disqualified addresses at rules-construction time, would eliminate the redundancy and make the relationship between the two fields explicit.

Copy link
Member

@lightwalker-eth lightwalker-eth left a comment

Choose a reason for hiding this comment

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

@Goader Nice update! Shared a few small comments. Please take the lead to merge when ready 👍

Comment on lines +18 to +21
/**
* An admin-imposed disqualification entry for a specific referrer in an edition.
*/
export interface ReferralProgramAdminDisqualification {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
/**
* An admin-imposed disqualification entry for a specific referrer in an edition.
*/
export interface ReferralProgramAdminDisqualification {
/**
* An admin-imposed disqualification of a specific referrer in an edition.
*/
export interface ReferralProgramEditionDisqualification {

Goal: Put more emphasis on how this is a disqualification for an edition over it coming from an admin.

rules: ReferralProgramRulesRevShareLimit,
): boolean {
return totalBaseRevenueContribution.amount >= rules.minQualifiedRevenueContribution.amount;
const normalizedReferrer = normalizeAddress(referrer);
Copy link
Member

Choose a reason for hiding this comment

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

We should guarantee these addresses are stored in normalized form (I assume this means fully lowercase?) so that when we're performing logic on these addresses all formatting is already guaranteed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

They are normalized by that point in any place this function is being called right now. But since we are exporting it and making it available for anyone to call, I figured it would be nice to have a guarantee on that too instead of stating that invariant in the documentation and putting the responsibility on the caller/user. What do you think?

* The reason for admin disqualification, or null if not disqualified.
*
* @invariant null when {@link isAdminDisqualified} is false.
* @invariant Non-null string when {@link isAdminDisqualified} is true.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
* @invariant Non-null string when {@link isAdminDisqualified} is true.
* @invariant Non-empty string when {@link isAdminDisqualified} is true.

Comment on lines +138 to +139
rules.disqualifications.find((d) => d.referrer === referrer.referrer) ?? null;

Copy link
Member

Choose a reason for hiding this comment

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

We should guarantee we store values in normalized form so that while processing those values we don't need to normalize again or check for normalization.

@Goader Goader requested a review from a team March 4, 2026 11:39
@vercel vercel bot temporarily deployed to Preview – admin.ensnode.io March 4, 2026 12:12 Inactive
@vercel vercel bot temporarily deployed to Preview – ensnode.io March 4, 2026 12:12 Inactive
@vercel vercel bot temporarily deployed to Preview – ensrainbow.io March 4, 2026 12:12 Inactive
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
packages/ens-referrals/src/v1/award-models/rev-share-limit/rules.ts (1)

158-166: ⚠️ Potential issue | 🟠 Major

Avoid linear disqualification scans in the qualification hot path.

Line 164 performs a linear .some() for every qualification check. During leaderboard construction this can become O(events × disqualifications) and degrade runtime noticeably on larger editions. Prefer a precomputed normalized Set lookup in event loops.

⚙️ Refactor direction
 export function isReferrerQualifiedRevShareLimit(
   referrer: Address,
   totalBaseRevenueContribution: PriceUsdc,
   rules: ReferralProgramRulesRevShareLimit,
+  disqualifiedReferrers: ReadonlySet<Address>,
 ): boolean {
   const normalizedReferrer = normalizeAddress(referrer);
-  const isAdminDisqualified = rules.disqualifications.some(
-    (d) => d.referrer === normalizedReferrer,
-  );
+  const isAdminDisqualified = disqualifiedReferrers.has(normalizedReferrer);
   return (
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ens-referrals/src/v1/award-models/rev-share-limit/rules.ts` around
lines 158 - 166, The qualification hot path uses
rules.disqualifications.some(...) inside isReferrerQualifiedRevShareLimit, which
causes O(events × disqualifications); change this to a constant-time Set lookup
by precomputing a normalized Set of disqualified referrer addresses (e.g.,
buildDisqualifiedReferrerSet or disqualifiedReferrersSet) and replace the
.some(...) test with disqualifiedReferrersSet.has(normalizedReferrer); ensure
the Set is built once (e.g., where rules are loaded or before leaderboard/event
loops) using normalizeAddress on each disqualification.referrer so
isReferrerQualifiedRevShareLimit only performs a Set.has call.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@packages/ens-referrals/src/v1/award-models/rev-share-limit/rules.ts`:
- Around line 158-166: The qualification hot path uses
rules.disqualifications.some(...) inside isReferrerQualifiedRevShareLimit, which
causes O(events × disqualifications); change this to a constant-time Set lookup
by precomputing a normalized Set of disqualified referrer addresses (e.g.,
buildDisqualifiedReferrerSet or disqualifiedReferrersSet) and replace the
.some(...) test with disqualifiedReferrersSet.has(normalizedReferrer); ensure
the Set is built once (e.g., where rules are loaded or before leaderboard/event
loops) using normalizeAddress on each disqualification.referrer so
isReferrerQualifiedRevShareLimit only performs a Set.has call.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 7b055887-5a01-4123-a6b8-28a1c12927ac

📥 Commits

Reviewing files that changed from the base of the PR and between dacc70d and cab9724.

📒 Files selected for processing (4)
  • packages/ens-referrals/src/v1/award-models/rev-share-limit/api/zod-schemas.ts
  • packages/ens-referrals/src/v1/award-models/rev-share-limit/leaderboard.test.ts
  • packages/ens-referrals/src/v1/award-models/rev-share-limit/metrics.ts
  • packages/ens-referrals/src/v1/award-models/rev-share-limit/rules.ts

@Goader Goader merged commit 3ece8f0 into main Mar 4, 2026
16 checks passed
@Goader Goader deleted the feat/referral-program-disqualification branch March 4, 2026 12:32
@github-actions github-actions bot mentioned this pull request Mar 4, 2026
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.

3 participants