Skip to content

rev-share-limit rules for the new Referral Program Edition#1663

Open
Goader wants to merge 19 commits intomainfrom
feat/referral-program-rules-editions
Open

rev-share-limit rules for the new Referral Program Edition#1663
Goader wants to merge 19 commits intomainfrom
feat/referral-program-rules-editions

Conversation

@Goader
Copy link
Contributor

@Goader Goader commented Feb 23, 2026

Introducing different Award Models (for future Referral Program Editions)

closes: #1649

Summary

  • Changed the structure of ens-referrals to allow for multiple award models (the rules of how awards are assigned) for future referral program editions
  • The original logic for the ENS Holiday Awards is now used as a pie-split model
  • Introduced the new rev-share-limit award model to support the next referral program edition

Why


Testing

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

Notes for Reviewer (Optional)

  • New package structure
  • Zod validation supporting forward compatibility
  • rev-share-limit leaderboard building logic

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 Feb 23, 2026
Copilot AI review requested due to automatic review settings February 23, 2026 14:25
@vercel
Copy link
Contributor

vercel bot commented Feb 23, 2026

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

Project Deployment Actions Updated (UTC)
admin.ensnode.io Ready Ready Preview, Comment Feb 27, 2026 4:45pm
ensnode.io Ready Ready Preview, Comment Feb 27, 2026 4:45pm
ensrainbow.io Ready Ready Preview, Comment Feb 27, 2026 4:45pm

@changeset-bot
Copy link

changeset-bot bot commented Feb 23, 2026

🦋 Changeset detected

Latest commit: 8d6ae5c

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

This PR includes changesets to release 19 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/ponder-metadata 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

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 23, 2026

📝 Walkthrough

Walkthrough

Introduces a pluggable award-model architecture for referral programs: adds PieSplit and RevShareLimit models (plus Unrecognized for forward compatibility), refactors types/schemas/serializers to an awardModel-discriminated union, and updates API handlers, builders, DB queries, mocks, and tests to dispatch per award model.

Changes

Cohort / File(s) Summary
Shared award-model foundation
packages/ens-referrals/src/v1/award-models/shared/..., packages/ens-referrals/src/v1/award-models/shared/rules.ts, .../leaderboard-page.ts, .../rank.ts, .../score.ts
Add discriminant ReferralProgramAwardModels, base rules, leaderboard-page context/ pagination helpers, rank/score utilities, and validators used by model variants.
Pie-Split model
packages/ens-referrals/src/v1/award-models/pie-split/...
New pie-split-specific modules: rules, metrics (scored/ranked/awarded/unranked), aggregations, leaderboard, leaderboard-page, edition-metrics, score, rank, Zod schemas, serialized types, and serializers. Mirrors existing Holiday Awards logic under PieSplit discriminant.
Rev-Share-Limit model
packages/ens-referrals/src/v1/award-models/rev-share-limit/...
New rev-share-limit model: rules (minQualifiedRevenueContribution, qualifiedRevenueShare), referral-event type, metrics pipeline, deterministic event-based leaderboard builder, aggregations with awardPoolRemaining, Zod schemas, serializers, and extensive unit tests.
API types & serialization
packages/ens-referrals/src/v1/api/serialized-types.ts, api/serialize.ts, zod-schemas.ts
Replace concrete serialized interfaces with union types per awardModel; implement per-model serializers and switch-based dispatch with exhaustive runtime guards.
Top-level API & public surface
packages/ens-referrals/src/v1/index.ts, rules.ts, referrer-metrics.ts, leaderboard.ts, leaderboard-page.ts, score.ts, rank.ts
Convert monolithic types to discriminated unions and reorganize exports to new award-model modules; remove legacy single-path builders and many legacy public types.
Edition defaults & config caching
packages/ens-referrals/src/v1/edition-defaults.ts, apps/ensapi/src/cache/referral-program-edition-set.cache.ts
Use model-specific rule builders for defaults; filter out Unrecognized editions at server load-time to avoid runtime handling of unknown models.
Database & handler changes
apps/ensapi/src/lib/ensanalytics/referrer-leaderboard/database-v1.ts, get-referrer-leaderboard-v1.ts, mocks-v1.ts
Add getReferralEvents (raw events) for rev-share-limit; getReferrerLeaderboard dispatches on rules.awardModel to call model-specific builders; mocks and tests updated to include awardModel fields.
Tests & CI
apps/ensapi/src/handlers/ensanalytics-api-v1.test.ts, .../get-referrer-leaderboard-v1.test.ts, .../zod-schemas.test.ts, .../rev-share-limit/leaderboard.test.ts, others
Update tests to use model-specific builders/ types (PieSplit/RevShareLimit), add comprehensive rev-share-limit leaderboard tests, and gate assertions on awardModel where necessary.
Serialization types per model
packages/ens-referrals/src/v1/award-models/*/api/serialized-types.ts, .../api/serialize.ts
Add model-specific serialized type definitions and serializers for price/date conversions and nested serialization.
Changelog & client docs
.changeset/..., packages/ens-referrals/src/v1/client.ts
Add changelog; document client behavior for unrecognized awardModel editions and guidance for callers.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Client
  participant API as ENSAPI Handler
  participant DB as Database (events/metrics)
  participant Builder as AwardModel Builder
  participant Serializer

  Client->>API: GET /referrer-leaderboard (edition rules)
  API->>DB: fetch edition rules (includes awardModel)
  alt awardModel == PieSplit
    API->>DB: getReferrerMetrics(rules)
    DB-->>API: aggregated metrics
    API->>Builder: buildReferrerLeaderboardPieSplit(metrics, rules, accurateAsOf)
  else awardModel == RevShareLimit
    API->>DB: getReferralEvents(rules)
    DB-->>API: ordered referral events
    API->>Builder: buildReferrerLeaderboardRevShareLimit(events, rules, accurateAsOf)
  else unrecognized
    API-->>Client: 400/422 (or throws / documented warning)
  end
  Builder-->>API: ReferrerLeaderboardPage (model-specific)
  API->>Serializer: serializeReferrerLeaderboardPage(page)
  Serializer-->>API: Serialized JSON
  API-->>Client: 200 { data: serializedPage }
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested labels

ensanalytics

Poem

🐰 A hop, a rule, a model new and bright,
Pie-split crumbs by day, rev-share through the night.
Unrecognized? I tip my ear and say "that's fine"—
New awards, new paths, each edition's line.
Hooray for flexible carrots on the vine!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title references a specific implementation detail (rev-share-limit rules) but is partially related to the broader change of introducing multiple award models for referral program editions.
Description check ✅ Passed The description covers the main changes, reasons, testing approach, and special notes. It follows the lite PR template with Summary, Why, Testing, and Notes sections appropriately filled out.
Linked Issues check ✅ Passed The PR addresses all key objectives from #1649: introduces discriminated union for award models with pie-split and rev-share-limit, updates ReferralProgramRules with model-specific fields, implements per-model AggregatedReferrerMetrics and ReferrerMetrics structures, supports different qualification strategies, and includes forward-compatible Zod validation.
Out of Scope Changes check ✅ Passed The changes are scoped to implementing the multi-award-model architecture specified in #1649, including pie-split/rev-share-limit models, Zod schemas, leaderboard logic, and related test files. No unrelated changes detected.
Docstring Coverage ✅ Passed Docstring coverage is 91.18% 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-rules-editions

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

Refactors @namehash/ens-referrals/v1 to support multiple referral-program award models (via an awardModel discriminant) and introduces the new rev-share-limit rules/model intended for the next Referral Program edition.

Changes:

  • Split the previous single award model into award-model-specific modules (pie-split vs rev-share-limit) with shared helpers.
  • Added rev-share-limit rules, leaderboard/metrics/aggregation pipeline, and API (zod schemas + serialization).
  • Updated defaults, API wiring, and ENSAPI mocks/tests to use the new discriminated types.

Reviewed changes

Copilot reviewed 47 out of 47 changed files in this pull request and generated 13 comments.

Show a summary per file
File Description
packages/ens-referrals/src/v1/score.ts Removed legacy shared score; score logic moved under award-model modules.
packages/ens-referrals/src/v1/rules.ts Replaced single rules interface with a discriminated union of award-model rule variants.
packages/ens-referrals/src/v1/referrer-metrics.ts Trimmed to DB-aggregated base metrics only; removed scoring/ranking/awarding logic.
packages/ens-referrals/src/v1/rank.ts Removed legacy shared ranking/scoring logic; replaced by shared + model-specific modules.
packages/ens-referrals/src/v1/leaderboard.ts Dispatches leaderboard building by rules.awardModel.
packages/ens-referrals/src/v1/leaderboard-page.ts Dispatches page building by leaderboard.awardModel; page-context moved to shared module.
packages/ens-referrals/src/v1/leaderboard-page.test.ts Updated tests to use pie-split-specific types/modules.
packages/ens-referrals/src/v1/index.ts Updated barrel exports to include award-model modules and shared helpers.
packages/ens-referrals/src/v1/edition-metrics.ts Now returns award-model-specific edition metrics variants keyed by awardModel.
packages/ens-referrals/src/v1/edition-defaults.ts Defaults now build pie-split vs rev-share-limit rules explicitly per edition.
packages/ens-referrals/src/v1/base-metrics.ts New “base metrics” duplicate type/functions (potentially intended as DB layer input).
packages/ens-referrals/src/v1/award-models/shared/score.ts Introduced shared ReferrerScore + validation.
packages/ens-referrals/src/v1/award-models/shared/rules.ts Added ReferralProgramAwardModels constants + base shared rule fields.
packages/ens-referrals/src/v1/award-models/shared/rank.ts Added shared referrer sorting + rank validation.
packages/ens-referrals/src/v1/award-models/shared/leaderboard-page.ts Moved pagination params/context + generic referrer slicing to shared module.
packages/ens-referrals/src/v1/award-models/shared/leaderboard-guards.ts Centralized shared leaderboard input invariants.
packages/ens-referrals/src/v1/award-models/shared/edition-metrics.ts Centralized ranked/unranked type discriminants.
packages/ens-referrals/src/v1/award-models/shared/api/zod-schemas.ts Added shared zod schemas for page context + status.
packages/ens-referrals/src/v1/award-models/rev-share-limit/rules.ts Added rev-share-limit rule shape + validation + builder.
packages/ens-referrals/src/v1/award-models/rev-share-limit/rank.ts Added rev-share-limit qualification helper (currently duplicated elsewhere).
packages/ens-referrals/src/v1/award-models/rev-share-limit/metrics.ts Added rev-share-limit metrics pipeline (base revenue contrib, qualification, award value).
packages/ens-referrals/src/v1/award-models/rev-share-limit/leaderboard.ts Added rev-share-limit leaderboard builder + shape.
packages/ens-referrals/src/v1/award-models/rev-share-limit/leaderboard-page.ts Added rev-share-limit leaderboard page shape + builder.
packages/ens-referrals/src/v1/award-models/rev-share-limit/edition-metrics.ts Added rev-share-limit edition metrics types.
packages/ens-referrals/src/v1/award-models/rev-share-limit/api/zod-schemas.ts Added rev-share-limit API zod schemas for rules/metrics/pages.
packages/ens-referrals/src/v1/award-models/rev-share-limit/api/serialized-types.ts Added serialized (JSON-safe) types for rev-share-limit API payloads.
packages/ens-referrals/src/v1/award-models/rev-share-limit/api/serialize.ts Added serializers for rev-share-limit rules/metrics/pages.
packages/ens-referrals/src/v1/award-models/rev-share-limit/aggregations.ts Added rev-share-limit aggregation + pool-scaling logic.
packages/ens-referrals/src/v1/award-models/pie-split/score.ts Reintroduced pie-split score calculation in model-specific module.
packages/ens-referrals/src/v1/award-models/pie-split/rules.ts Reintroduced pie-split rules + builder/validation in model-specific module.
packages/ens-referrals/src/v1/award-models/pie-split/rank.ts Reintroduced pie-split qualification + score boost/final score logic.
packages/ens-referrals/src/v1/award-models/pie-split/metrics.ts Reintroduced pie-split scored/ranked/awarded metrics pipeline.
packages/ens-referrals/src/v1/award-models/pie-split/leaderboard.ts Added pie-split leaderboard builder + shape under award-model namespace.
packages/ens-referrals/src/v1/award-models/pie-split/leaderboard-page.ts Added pie-split leaderboard page shape + builder under award-model namespace.
packages/ens-referrals/src/v1/award-models/pie-split/edition-metrics.ts Added pie-split edition metrics types under award-model namespace.
packages/ens-referrals/src/v1/award-models/pie-split/api/zod-schemas.ts Added pie-split API zod schemas for rules/metrics/pages.
packages/ens-referrals/src/v1/award-models/pie-split/api/serialized-types.ts Added serialized (JSON-safe) types for pie-split API payloads.
packages/ens-referrals/src/v1/award-models/pie-split/api/serialize.ts Added serializers for pie-split rules/metrics/pages.
packages/ens-referrals/src/v1/award-models/pie-split/aggregations.ts Refactored existing aggregation logic into model-specific module.
packages/ens-referrals/src/v1/api/zod-schemas.ts Updated top-level API schemas to union known award models + allow unknown awardModel passthrough for forward compatibility.
packages/ens-referrals/src/v1/api/types.ts Updated API types to import pagination params from shared module.
packages/ens-referrals/src/v1/api/serialized-types.ts Updated top-level serialized types to union model variants and add an “unknown rules” fallback.
packages/ens-referrals/src/v1/api/serialize.ts Updated serialization to dispatch by awardModel into model-specific serializers.
packages/ens-referrals/src/v1/api/deserialize.ts Added type assertions to accommodate widened zod schema outputs (passthrough unknown award models).
apps/ensapi/src/lib/ensanalytics/referrer-leaderboard/mocks-v1.ts Updated mocks to include awardModel and pie-split-specific types.
apps/ensapi/src/lib/ensanalytics/referrer-leaderboard/get-referrer-leaderboard-v1.test.ts Updated tests to use pie-split rule builder/types; adjusted assertions for model-specific fields.
apps/ensapi/src/handlers/ensanalytics-api-v1.test.ts Updated handler tests to use pie-split rule builder/types and include awardModel in responses.

💡 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: 25

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
packages/ens-referrals/src/v1/edition-defaults.ts (1)

44-57: 🧹 Nitpick | 🔵 Trivial

Edition 2 configuration looks correct; TODO for the rules URL is noted.

The parameter mapping to buildReferralProgramRulesRevShareLimit aligns with the builder signature. The placeholder rules URL (line 55) reusing the Holiday Awards URL is flagged by the TODO — just make sure this is resolved before merge.

Would you like me to open an issue to track updating the March 2026 rules URL?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ens-referrals/src/v1/edition-defaults.ts` around lines 44 - 57,
Edition 2 uses a placeholder rules URL (the Holiday Awards URL) in the rules
parameter for the edition2 ReferralProgramEditionConfig; locate the edition2
object and the buildReferralProgramRulesRevShareLimit invocation and replace the
new URL("https://ensawards.org/ens-holiday-awards-rules") argument with the
official March 2026 rules URL once published, and remove the TODO comment (or
create a tracked issue and reference its ID in a short comment if the URL is not
yet available).
apps/ensapi/src/handlers/ensanalytics-api-v1.test.ts (1)

452-471: 🧹 Nitpick | 🔵 Trivial

Inconsistent casting: edition1 is cast to UnrankedReferrerMetricsPieSplit but edition2 is not.

Lines 452–465 cast edition1.referrer to UnrankedReferrerMetricsPieSplit and assert PieSplit-specific fields (finalScoreBoost, finalScore, awardPoolShare, awardPoolApproxValue). But lines 468–471 access edition2.referrer fields without casting and only check referrer and rank. If the intent is to validate both editions equally, apply the same cast and assertions to edition2.

Also, if you do keep the abbreviated check for edition2 as a deliberate lighter-weight smoke test, the pattern is acceptable—but a short comment explaining that would help future readers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ensapi/src/handlers/ensanalytics-api-v1.test.ts` around lines 452 - 471,
The test inconsistently casts edition1.referrer to
UnrankedReferrerMetricsPieSplit but leaves edition2.referrer uncast; fix by
casting edition2.referrer the same way (e.g., const edition2Referrer =
edition2.referrer as UnrankedReferrerMetricsPieSplit) and replicate the same
PieSplit assertions (finalScoreBoost, finalScore, awardPoolShare,
awardPoolApproxValue, isQualified, total fields, etc.) and the accurateAsOf
check, or if you intend a lighter smoke test for edition2 add a short inline
comment explaining that difference; reference variables edition1, edition2 and
the UnrankedReferrerMetricsPieSplit type and ReferrerEditionMetricsTypeIds in
the change.
packages/ens-referrals/src/v1/edition-metrics.ts (1)

44-53: 🧹 Nitpick | 🔵 Trivial

Remove the redundant @returns tag.

The @returns on line 52 restates what lines 47–48 already describe. As per coding guidelines, "Do not add JSDoc @returns tags that merely restate the method summary; remove redundancy during PR review."

Proposed fix
 /**
  * Get the edition metrics for a specific referrer from the leaderboard.
  *
  * Returns a {`@link` ReferrerEditionMetricsRanked} if the referrer is on the leaderboard,
  * or a {`@link` ReferrerEditionMetricsUnranked} if the referrer has no referrals.
  *
  * `@param` referrer - The referrer address to look up
  * `@param` leaderboard - The referrer leaderboard to query
- * `@returns` The appropriate {`@link` ReferrerEditionMetrics} (ranked or unranked)
  */
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ens-referrals/src/v1/edition-metrics.ts` around lines 44 - 53, The
JSDoc for the "Get the edition metrics for a specific referrer" function
contains a redundant `@returns` tag that repeats the summary; remove the `@returns`
tag from the JSDoc block (the one that describes returning
ReferrerEditionMetricsRanked or ReferrerEditionMetricsUnranked) so the summary
and the type info in the main description remain and no duplicate `@returns` entry
exists; update the comment above the function (the block starting with "Get the
edition metrics for a specific referrer from the leaderboard.") to delete the
redundant `@returns` line.
♻️ Duplicate comments (4)
packages/ens-referrals/src/v1/award-models/rev-share-limit/rank.ts (1)

11-16: Duplicate of isReferrerQualifiedRevShareLimit in rev-share-limit/rules.ts (lines 132–136).

Same function name, parameters, and body exist in both files. This was already flagged in the review of rules.ts. Keep the canonical definition in one file (likely rank.ts given the module's responsibility) and re-export or import from there in the other.

🤖 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/rank.ts` around
lines 11 - 16, There is a duplicate function definition for
isReferrerQualifiedRevShareLimit (same signature and body) across the module;
remove the redundant copy and keep a single canonical implementation (preferably
the one in rank.ts), then update the other location (rules.ts) to import and
re-export or call that canonical function instead of redefining it; ensure all
references use the single exported function to avoid duplication and
inconsistency.
packages/ens-referrals/src/v1/api/deserialize.ts (3)

59-60: Same as unknown as T concern as raised on lines 37–40. This pattern appears identically in all four deserializer functions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ens-referrals/src/v1/api/deserialize.ts` around lines 59 - 60, The
deserializers are using a double-cast "as unknown as T" (e.g., the return line
returning parsed.data as unknown as ReferrerMetricsEditionsResponse) which
defeats type safety; to fix, give the parser/deserialize helper a proper generic
or typed return so parsed is typed correctly (or explicitly validate the shape)
and then return parsed.data directly as ReferrerMetricsEditionsResponse (remove
the "as unknown as" pattern). Update the deserializer functions in this file to
either call the parser with the correct generic type or add a runtime/type guard
that narrows parsed.data before returning, replacing lines like "return
parsed.data as unknown as ReferrerMetricsEditionsResponse" with a properly
typed/validated return.

79-80: Same as unknown as T concern as raised on lines 37–40.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ens-referrals/src/v1/api/deserialize.ts` around lines 79 - 80, The
double cast "as unknown as ReferralProgramEditionConfig[]" on the return of
parsed.data is unsafe—replace the passthrough widen cast with a proper typed
conversion or runtime validation: ensure the parser/deserialize function that
produces parsed (the parsed variable in this file) either returns a correctly
typed shape or run a lightweight validation/assertion against
ReferralProgramEditionConfig[] before casting; update the return to use a
single, justified cast only after validation (or return the validated value
directly) so you remove the "as unknown as" pattern and refer to the parsed.data
and ReferralProgramEditionConfig symbols for locating the change.

99-100: Same as unknown as T concern as raised on lines 37–40.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ens-referrals/src/v1/api/deserialize.ts` around lines 99 - 100, The
return currently uses an unsafe double-cast "parsed.data as unknown as
ReferralProgramEditionConfigSetResponse" — replace this with a real runtime
validation/narrowing step instead of the `as unknown as` bypass: validate
parsed.data against the expected shape (e.g., with an existing Zod/validator or
a new helper like validateReferralProgramEditionConfigSetResponse) and either
throw on invalid input or map/transform parsed.data into a properly typed
ReferralProgramEditionConfigSetResponse before returning; reference the parsed
variable and the ReferralProgramEditionConfigSetResponse type and remove the `as
unknown as` cast.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/ensapi/src/handlers/ensanalytics-api-v1.test.ts`:
- Line 534: The variable name edition1Referrer2 is confusing and should be
renamed to edition1Referrer; update the declaration that casts edition1.referrer
(currently "const edition1Referrer2 = edition1.referrer as
UnrankedReferrerMetricsPieSplit") and any subsequent references to use
edition1Referrer so it matches the pattern used elsewhere in the tests (e.g.,
the previous test's edition1Referrer) and improves readability.

In
`@apps/ensapi/src/lib/ensanalytics/referrer-leaderboard/get-referrer-leaderboard-v1.test.ts`:
- Around line 64-104: Narrow the leaderboard result once instead of repeating
casts: first assert result.awardModel === "pie-split", then cast the leaderboard
result to ReferrerLeaderboardPieSplit so its entries are typed as
AwardedReferrerMetricsPieSplit; update the test setup (where you build
qualifiedReferrers/unqualifiedReferrers) to use that narrowed type and remove
the repeated (r as AwardedReferrerMetricsPieSplit) casts in the checks for
finalScoreBoost, finalScore, and awardPoolShare.
- Around line 76-88: The assertion for qualified referrers incorrectly expects
finalScore === score * finalScoreBoost; update the test to match
calcReferrerFinalScorePieSplit's formula by asserting finalScore === score * (1
+ finalScoreBoost) (or use the multiplier helper
calcReferrerFinalScoreMultiplierPieSplit if available) for entries in
qualifiedReferrers (cast as AwardedReferrerMetricsPieSplit), and keep the
unqualifiedReferrers assertion as-is (finalScore === score).

In `@packages/ens-referrals/src/v1/api/deserialize.ts`:
- Around line 37-40: The current cast "parsed.data as unknown as
ReferrerLeaderboardPageResponse" hides structural mismatches between the Zod
passthrough schema and the public union type and causes runtime failures (see
serializeReferrerLeaderboardPage); fix by giving the Zod schema an explicit
return type so the mismatch is surfaced: in zod-schemas.ts change
makeReferrerLeaderboardPageResponseSchema to return
z.ZodType<ReferrerLeaderboardPageResponse> (or the appropriate public type),
remove the unsafe cast in deserialize.ts, then resolve the compile error by
either (A) adding an explicit "unknown" variant to the public types (and
handling it in serializeReferrerLeaderboardPage and downstream code) or (B)
removing passthrough from the schema so unknown award models are rejected;
implement the chosen option and update
serializeReferrerLeaderboardPage/ReferralProgramRules handling accordingly.

In `@packages/ens-referrals/src/v1/api/zod-schemas.ts`:
- Around line 41-63: The union's final passthrough currently accepts any object
with awardModel:string and will mask validation failures from known variants;
update makeReferralProgramRulesSchema and makeReferrerLeaderboardPageSchema so
the catch-all explicitly excludes known awardModel values (or validates
awardModel literal first) before falling back to passthrough. Locate the known
variant factories (makeReferralProgramRulesPieSplitSchema,
makeReferralProgramRulesRevShareLimitSchema,
makeReferrerLeaderboardPagePieSplitSchema,
makeReferrerLeaderboardPageRevShareLimitSchema) and either (a) change the
catch-all to refine its awardModel string to ensure it is not one of those known
literals, or (b) validate awardModel as a literal/enum and only if it is unknown
allow .passthrough(); that way malformed known-variant payloads will fail the
appropriate variant validation instead of being swallowed.

In `@packages/ens-referrals/src/v1/award-models/pie-split/api/serialize.ts`:
- Around line 56-93: Both serializeAwardedReferrerMetricsPieSplit and
serializeUnrankedReferrerMetricsPieSplit duplicate the same field mapping;
extract the shared mapping into a helper (e.g.,
serializeCommonReferrerMetricsPieSplit or serializeReferrerMetricsBase) that
accepts the common input type (AwardedReferrerMetricsPieSplit |
UnrankedReferrerMetricsPieSplit or a shared base interface) and returns the
common Serialized fields (referrer, totalReferrals, totalIncrementalDuration,
totalRevenueContribution via serializePriceEth, score, rank, isQualified,
finalScoreBoost, finalScore, awardPoolShare, awardPoolApproxValue via
serializePriceUsdc); then have serializeAwardedReferrerMetricsPieSplit and
serializeUnrankedReferrerMetricsPieSplit call that helper and merge or return
its result (adding any model-specific fields later if needed) to remove
duplication.

In `@packages/ens-referrals/src/v1/award-models/pie-split/api/zod-schemas.ts`:
- Around line 47-94: The two schemas makeAwardedReferrerMetricsPieSplitSchema
and makeUnrankedReferrerMetricsPieSplitSchema duplicate most fields—extract the
common fields into a reusable base schema factory (e.g.,
makeBaseReferrerMetricsPieSplitSchema(valueLabel)) that returns a z.object
containing referrer, totalReferrals, totalIncrementalDuration,
totalRevenueContribution, score, finalScoreBoost, finalScore, awardPoolShare,
and awardPoolApproxValue (preserving the existing validators and error
messages), then update makeAwardedReferrerMetricsPieSplitSchema to return
makeBaseReferrerMetricsPieSplitSchema(valueLabel).extend({ rank:
makePositiveIntegerSchema(...), isQualified: z.boolean() }) and update
makeUnrankedReferrerMetricsPieSplitSchema to return
makeBaseReferrerMetricsPieSplitSchema(valueLabel).extend({ rank: z.null(),
isQualified: z.literal(false) }); ensure valueLabel is forwarded and max
constraints remain intact.

In `@packages/ens-referrals/src/v1/award-models/pie-split/metrics.ts`:
- Around line 57-60: Update the stale JSDoc reference: change the {`@link`
ScoredReferrerMetrics} tag to {`@link` ScoredReferrerMetricsPieSplit} in the JSDoc
block that documents the PieSplit award model metrics so it matches the actual
type name (replace the link where ScoredReferrerMetrics is referenced alongside
ReferrerLeaderboardPieSplit and ReferralProgramRulesPieSplit).
- Around line 168-171: Update the stale JSDoc references that point to the
non-existent AggregatedRankedReferrerMetricsPieSplit: replace occurrences in the
comment block extending RankedReferrerMetricsPieSplit and the `@invariant` that
reference AggregatedRankedReferrerMetricsPieSplit with the correct
AggregatedReferrerMetricsPieSplit (the type imported at top). Ensure the JSDoc
{`@link` ...} and any textual mentions use AggregatedReferrerMetricsPieSplit so
the docs and invariants correctly reference the existing type.
- Around line 247-323: The validateUnrankedReferrerMetricsPieSplit function
currently uses price eth/usdc schemas with safeParse + manual throw
(ethParseResult/usdcParseResult); change these to call
priceEthSchema.parse(metrics.totalRevenueContribution) and
priceUsdcSchema.parse(metrics.awardPoolApproxValue) so Zod throws automatically
on failure and remove the corresponding manual if-checks that inspect .success
and .error; keep the subsequent checks that assert .amount === 0n and the rest
of the validations unchanged.

In `@packages/ens-referrals/src/v1/award-models/pie-split/rules.ts`:
- Around line 32-64: Extract the duplicated BaseReferralProgramRules checks into
a new helper validateBaseReferralProgramRules in shared/rules.ts that validates
subregistryId (use makeAccountIdSchema as in
validateReferralProgramRulesPieSplit), startTime and endTime via
validateUnixTimestamp, rulesUrl instanceof URL, and the endTime >= startTime
invariant; then remove those checks from validateReferralProgramRulesPieSplit
and validateReferralProgramRulesRevShareLimit and call
validateBaseReferralProgramRules(rules) from each function so both reuse the
same logic.

In `@packages/ens-referrals/src/v1/award-models/pie-split/score.ts`:
- Around line 6-18: The JSDoc for calcReferrerScorePieSplit is edition-specific
("ENS Holiday Awards period"); update the comment to use generic wording (e.g.,
"within the referral program edition" or "within the PieSplit referral program
period") so the utility remains model-generic; modify the description and any
`@param/`@returns text in the calcReferrerScorePieSplit JSDoc to remove "ENS
Holiday Awards" and reference the generic ReferralProgramRulesPieSplit or
"referral program edition" instead.

In `@packages/ens-referrals/src/v1/award-models/rev-share-limit/aggregations.ts`:
- Around line 50-75: Replace the verbose safeParse + manual throw pattern in
validateAggregatedReferrerMetricsRevShareLimit with zod's throwing parse: call
makePriceEthSchema(...).parse(metrics.grandTotalRevenueContribution) and
makePriceUsdcSchema(...).parse(metrics.awardPoolRemaining) instead of using
safeParse and constructing new Error, so the native ZodError is thrown for
invalid grandTotalRevenueContribution and awardPoolRemaining; keep existing
validateNonNegativeInteger and validateDuration checks unchanged.
- Around line 117-120: The current computation of scalingFactor converts bigints
to Number which loses precision for values > Number.MAX_SAFE_INTEGER; update the
logic around scalingFactor, totalPotentialAwardsAmount and
rules.totalAwardPoolValue.amount to avoid unsafe Number conversions: add a guard
that checks if either bigint > Number.MAX_SAFE_INTEGER and, if so, compute the
ratio using integer arithmetic (e.g. compare rules.totalAwardPoolValue.amount >=
totalPotentialAwardsAmount to short-circuit to 1, otherwise compute a
fixed-point bigint division by multiplying numerator by a chosen scale factor
and dividing by denominator to produce a safe Number, or use a Big/decimal
library), otherwise fall back to the existing Number(...) division; ensure the
code references scalingFactor, totalPotentialAwardsAmount and
rules.totalAwardPoolValue.amount so reviewers can find and replace the
conversion logic.

In `@packages/ens-referrals/src/v1/award-models/rev-share-limit/api/serialize.ts`:
- Around line 59-90: Both serializers
serializeAwardedReferrerMetricsRevShareLimit and
serializeUnrankedReferrerMetricsRevShareLimit duplicate the same field mappings;
extract a single helper (e.g., serializeReferrerMetricsRevShareLimit) that
accepts an AwardedReferrerMetricsRevShareLimit (or the common base type) and
returns the shared Serialized... object by calling
serializePriceEth/serializePriceUsdc for the same fields, then have both
serializeAwardedReferrerMetricsRevShareLimit and
serializeUnrankedReferrerMetricsRevShareLimit delegate to that helper to remove
the duplication while keeping function signatures intact.

In
`@packages/ens-referrals/src/v1/award-models/rev-share-limit/api/zod-schemas.ts`:
- Around line 29-47: Replace the hardcoded string literal "rev-share-limit" used
in the z.object for awardModel with the constant
ReferralProgramAwardModels.RevShareLimit (i.e., change
z.literal("rev-share-limit") to
z.literal(ReferralProgramAwardModels.RevShareLimit)); ensure the
ReferralProgramAwardModels symbol is imported where this schema is declared so
the constant is available.

In
`@packages/ens-referrals/src/v1/award-models/rev-share-limit/leaderboard-page.ts`:
- Around line 45-59: The two builders buildLeaderboardPageRevShareLimit and
buildLeaderboardPagePieSplit are identical; refactor by introducing a single
generic helper (e.g., buildLeaderboardPage<T>) that calls
calcReferralProgramStatus and assembles the return object using sliceReferrers,
awardModel, rules, aggregatedMetrics, pageContext and accurateAsOf, then update
both buildLeaderboardPageRevShareLimit and buildLeaderboardPagePieSplit to
delegate to that helper to remove duplication while preserving typing and future
divergence.

In `@packages/ens-referrals/src/v1/award-models/rev-share-limit/leaderboard.ts`:
- Around line 40-53: The JSDoc for the referrers map contains copy-paste errors:
replace the incorrect type name `AwardedReferrerMetricsPieSplit` with
`AwardedReferrerMetricsRevShareLimit` in the top description, and update
invariant text references to use `totalBaseRevenueContribution` instead of the
non-existent `score` (ensure the invariant that each value is non-zero lists
`totalReferrals`, `totalIncrementalDuration`, and
`totalBaseRevenueContribution`); keep references to `referrers`, `Address`, and
`AwardedReferrerMetricsRevShareLimit` consistent in the comment.

In `@packages/ens-referrals/src/v1/award-models/rev-share-limit/metrics.ts`:
- Around line 121-133: The metric builders for RevShareLimit (e.g.,
buildUnrankedReferrerMetricsRevShareLimit and the ranked/awarded builders)
currently lack runtime invariant checks; either add validation functions
analogous to PieSplit's validateScoredReferrerMetricsPieSplit /
validateRankedReferrerMetricsPieSplit / validateUnrankedReferrerMetricsPieSplit
that assert invariants (e.g., rank === null, isQualified === false, revenue
types are priceUsdc/priceEth zeros where expected) and call them from each
builder, or add a clear comment/docstring on each builder explaining the
intentional omission and the invariants are enforced elsewhere; update the
builders to call the new validators to ensure runtime enforcement if you choose
the validation route.

In `@packages/ens-referrals/src/v1/award-models/rev-share-limit/rules.ts`:
- Around line 50-99: The validateReferralProgramRulesRevShareLimit function
currently uses safeParse + manual throw for poolSchema, minSchema and
accountIdSchema; replace those patterns with zod.parse() so validation throws
automatically: call poolSchema.parse(rules.totalAwardPoolValue),
minSchema.parse(rules.minQualifiedRevenueContribution) and
accountIdSchema.parse(rules.subregistryId) (schemas created by
makePriceUsdcSchema and makeAccountIdSchema respectively) and remove the
corresponding if (!...success) throw blocks, leaving the rest of the function
(qualifiedRevenueShare, timestamp checks, rulesUrl and start/end ordering)
unchanged.
- Around line 71-75: The current range check on rules.qualifiedRevenueShare lets
NaN pass because (NaN < 0) and (NaN > 1) are false; update the validation around
rules.qualifiedRevenueShare to explicitly reject non-numeric values (e.g., use
Number.isFinite or !Number.isNaN) before the existing bounds check and throw the
same ReferralProgramRulesRevShareLimit error with the invalid value; locate the
block that throws `ReferralProgramRulesRevShareLimit: qualifiedRevenueShare ...`
and add a guard like `if (!Number.isFinite(rules.qualifiedRevenueShare)) throw
new Error(...)` prior to the < 0 / > 1 checks.
- Around line 132-137: Remove the duplicate function
isReferrerQualifiedRevShareLimit from rank.ts and instead re-export the single
implementation that lives in rules.ts; specifically delete the function
declaration in rank.ts and add a re-export statement in rank.ts that forwards
isReferrerQualifiedRevShareLimit from rules.ts so existing imports (e.g.,
metrics.ts) continue to work without changing their import paths.

In `@packages/ens-referrals/src/v1/award-models/shared/api/zod-schemas.ts`:
- Around line 35-36: The function makeReferralProgramStatusSchema currently
accepts an unused parameter _valueLabel; either remove this parameter from the
signature to avoid confusion, or keep it for API consistency but add a concise
inline comment in the makeReferralProgramStatusSchema declaration noting that
_valueLabel is intentionally unused (keeps parity with other make*Schema
builders) so readers understand the purpose; reference
makeReferralProgramStatusSchema and the _valueLabel parameter when making the
change.

In `@packages/ens-referrals/src/v1/award-models/shared/leaderboard-page.ts`:
- Around line 129-134: The validator for totalPages currently accepts 0 because
it calls isNonNegativeInteger; change the check to require a positive integer
(>=1) by using or implementing isPositiveInteger (or otherwise enforcing > 0)
when validating context.totalPages, and update the thrown Error message to say
"totalPages must be a positive integer (>= 1)" to match the `@invariant` and the
builder logic (references: the totalPages validation block, context.totalPages,
and the following expectedTotalPages check in leaderboard-page.ts).

In `@packages/ens-referrals/src/v1/base-metrics.ts`:
- Line 8: The import of ReferralProgramRules in base-metrics.ts is only used as
a type in JSDoc and should be a type-only import to avoid bringing a runtime
dependency; replace the current import statement with a type import (e.g.,
import type { ReferralProgramRules } from "./rules";) so ReferralProgramRules is
erased at runtime and update any JSDoc references if necessary to ensure they
still resolve.

---

Outside diff comments:
In `@apps/ensapi/src/handlers/ensanalytics-api-v1.test.ts`:
- Around line 452-471: The test inconsistently casts edition1.referrer to
UnrankedReferrerMetricsPieSplit but leaves edition2.referrer uncast; fix by
casting edition2.referrer the same way (e.g., const edition2Referrer =
edition2.referrer as UnrankedReferrerMetricsPieSplit) and replicate the same
PieSplit assertions (finalScoreBoost, finalScore, awardPoolShare,
awardPoolApproxValue, isQualified, total fields, etc.) and the accurateAsOf
check, or if you intend a lighter smoke test for edition2 add a short inline
comment explaining that difference; reference variables edition1, edition2 and
the UnrankedReferrerMetricsPieSplit type and ReferrerEditionMetricsTypeIds in
the change.

In `@packages/ens-referrals/src/v1/edition-defaults.ts`:
- Around line 44-57: Edition 2 uses a placeholder rules URL (the Holiday Awards
URL) in the rules parameter for the edition2 ReferralProgramEditionConfig;
locate the edition2 object and the buildReferralProgramRulesRevShareLimit
invocation and replace the new
URL("https://ensawards.org/ens-holiday-awards-rules") argument with the official
March 2026 rules URL once published, and remove the TODO comment (or create a
tracked issue and reference its ID in a short comment if the URL is not yet
available).

In `@packages/ens-referrals/src/v1/edition-metrics.ts`:
- Around line 44-53: The JSDoc for the "Get the edition metrics for a specific
referrer" function contains a redundant `@returns` tag that repeats the summary;
remove the `@returns` tag from the JSDoc block (the one that describes returning
ReferrerEditionMetricsRanked or ReferrerEditionMetricsUnranked) so the summary
and the type info in the main description remain and no duplicate `@returns` entry
exists; update the comment above the function (the block starting with "Get the
edition metrics for a specific referrer from the leaderboard.") to delete the
redundant `@returns` line.

---

Duplicate comments:
In `@packages/ens-referrals/src/v1/api/deserialize.ts`:
- Around line 59-60: The deserializers are using a double-cast "as unknown as T"
(e.g., the return line returning parsed.data as unknown as
ReferrerMetricsEditionsResponse) which defeats type safety; to fix, give the
parser/deserialize helper a proper generic or typed return so parsed is typed
correctly (or explicitly validate the shape) and then return parsed.data
directly as ReferrerMetricsEditionsResponse (remove the "as unknown as"
pattern). Update the deserializer functions in this file to either call the
parser with the correct generic type or add a runtime/type guard that narrows
parsed.data before returning, replacing lines like "return parsed.data as
unknown as ReferrerMetricsEditionsResponse" with a properly typed/validated
return.
- Around line 79-80: The double cast "as unknown as
ReferralProgramEditionConfig[]" on the return of parsed.data is unsafe—replace
the passthrough widen cast with a proper typed conversion or runtime validation:
ensure the parser/deserialize function that produces parsed (the parsed variable
in this file) either returns a correctly typed shape or run a lightweight
validation/assertion against ReferralProgramEditionConfig[] before casting;
update the return to use a single, justified cast only after validation (or
return the validated value directly) so you remove the "as unknown as" pattern
and refer to the parsed.data and ReferralProgramEditionConfig symbols for
locating the change.
- Around line 99-100: The return currently uses an unsafe double-cast
"parsed.data as unknown as ReferralProgramEditionConfigSetResponse" — replace
this with a real runtime validation/narrowing step instead of the `as unknown
as` bypass: validate parsed.data against the expected shape (e.g., with an
existing Zod/validator or a new helper like
validateReferralProgramEditionConfigSetResponse) and either throw on invalid
input or map/transform parsed.data into a properly typed
ReferralProgramEditionConfigSetResponse before returning; reference the parsed
variable and the ReferralProgramEditionConfigSetResponse type and remove the `as
unknown as` cast.

In `@packages/ens-referrals/src/v1/award-models/rev-share-limit/rank.ts`:
- Around line 11-16: There is a duplicate function definition for
isReferrerQualifiedRevShareLimit (same signature and body) across the module;
remove the redundant copy and keep a single canonical implementation (preferably
the one in rank.ts), then update the other location (rules.ts) to import and
re-export or call that canonical function instead of redefining it; ensure all
references use the single exported function to avoid duplication and
inconsistency.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9bffd55 and 4829b18.

📒 Files selected for processing (47)
  • apps/ensapi/src/handlers/ensanalytics-api-v1.test.ts
  • apps/ensapi/src/lib/ensanalytics/referrer-leaderboard/get-referrer-leaderboard-v1.test.ts
  • apps/ensapi/src/lib/ensanalytics/referrer-leaderboard/mocks-v1.ts
  • packages/ens-referrals/src/v1/api/deserialize.ts
  • packages/ens-referrals/src/v1/api/serialize.ts
  • packages/ens-referrals/src/v1/api/serialized-types.ts
  • packages/ens-referrals/src/v1/api/types.ts
  • packages/ens-referrals/src/v1/api/zod-schemas.ts
  • packages/ens-referrals/src/v1/award-models/pie-split/aggregations.ts
  • packages/ens-referrals/src/v1/award-models/pie-split/api/serialize.ts
  • packages/ens-referrals/src/v1/award-models/pie-split/api/serialized-types.ts
  • packages/ens-referrals/src/v1/award-models/pie-split/api/zod-schemas.ts
  • packages/ens-referrals/src/v1/award-models/pie-split/edition-metrics.ts
  • packages/ens-referrals/src/v1/award-models/pie-split/leaderboard-page.ts
  • packages/ens-referrals/src/v1/award-models/pie-split/leaderboard.ts
  • packages/ens-referrals/src/v1/award-models/pie-split/metrics.ts
  • packages/ens-referrals/src/v1/award-models/pie-split/rank.ts
  • packages/ens-referrals/src/v1/award-models/pie-split/rules.ts
  • packages/ens-referrals/src/v1/award-models/pie-split/score.ts
  • packages/ens-referrals/src/v1/award-models/rev-share-limit/aggregations.ts
  • packages/ens-referrals/src/v1/award-models/rev-share-limit/api/serialize.ts
  • packages/ens-referrals/src/v1/award-models/rev-share-limit/api/serialized-types.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/edition-metrics.ts
  • packages/ens-referrals/src/v1/award-models/rev-share-limit/leaderboard-page.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/rank.ts
  • packages/ens-referrals/src/v1/award-models/rev-share-limit/rules.ts
  • packages/ens-referrals/src/v1/award-models/shared/api/zod-schemas.ts
  • packages/ens-referrals/src/v1/award-models/shared/edition-metrics.ts
  • packages/ens-referrals/src/v1/award-models/shared/leaderboard-guards.ts
  • packages/ens-referrals/src/v1/award-models/shared/leaderboard-page.ts
  • packages/ens-referrals/src/v1/award-models/shared/rank.ts
  • packages/ens-referrals/src/v1/award-models/shared/rules.ts
  • packages/ens-referrals/src/v1/award-models/shared/score.ts
  • packages/ens-referrals/src/v1/base-metrics.ts
  • packages/ens-referrals/src/v1/edition-defaults.ts
  • packages/ens-referrals/src/v1/edition-metrics.ts
  • packages/ens-referrals/src/v1/index.ts
  • packages/ens-referrals/src/v1/leaderboard-page.test.ts
  • packages/ens-referrals/src/v1/leaderboard-page.ts
  • packages/ens-referrals/src/v1/leaderboard.ts
  • packages/ens-referrals/src/v1/rank.ts
  • packages/ens-referrals/src/v1/referrer-metrics.ts
  • packages/ens-referrals/src/v1/rules.ts
  • packages/ens-referrals/src/v1/score.ts
💤 Files with no reviewable changes (2)
  • packages/ens-referrals/src/v1/rank.ts
  • packages/ens-referrals/src/v1/score.ts

@vercel vercel bot temporarily deployed to Preview – admin.ensnode.io February 23, 2026 23:12 Inactive
@vercel vercel bot temporarily deployed to Preview – ensrainbow.io February 23, 2026 23:12 Inactive
@vercel vercel bot temporarily deployed to Preview – ensnode.io February 23, 2026 23: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.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/ensapi/src/lib/ensanalytics/referrer-leaderboard/get-referrer-leaderboard-v1.test.ts (1)

48-51: ⚠️ Potential issue | 🟠 Major

Materialize referrers before repeated assertions.
Map.entries() returns a single-use iterator; take() + drop() over the same iterator combined with multiple .every() calls makes later assertions vacuously true and can skip entries. Convert to arrays once, then slice.

Suggested fix
-      const referrers = result.referrers.entries();
-      const qualifiedReferrers = referrers.take(rules.maxQualifiedReferrers);
-      const unqualifiedReferrers = referrers.drop(rules.maxQualifiedReferrers);
+      const referrers = Array.from(result.referrers.entries());
+      const qualifiedReferrers = referrers.slice(0, rules.maxQualifiedReferrers);
+      const unqualifiedReferrers = referrers.slice(rules.maxQualifiedReferrers);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/ensapi/src/lib/ensanalytics/referrer-leaderboard/get-referrer-leaderboard-v1.test.ts`
around lines 48 - 51, The test currently calls result.referrers.entries() which
returns a single-use iterator and then calls take()/drop() and multiple .every()
assertions against it; materialize the iterator into an array first to avoid
vacuous/consumed iterations — e.g., assign const referrerArray =
Array.from(result.referrers.entries()), then derive qualifiedReferrers and
unqualifiedReferrers by slicing with rules.maxQualifiedReferrers (e.g.,
referrerArray.slice(0, rules.maxQualifiedReferrers) and
referrerArray.slice(rules.maxQualifiedReferrers)), and use those arrays for all
subsequent assertions (references: referrers, qualifiedReferrers,
unqualifiedReferrers, rules.maxQualifiedReferrers).
♻️ Duplicate comments (9)
packages/ens-referrals/src/v1/award-models/rev-share-limit/aggregations.ts (1)

42-67: Use zod.parse(...) instead of safeParse + manual throw — already flagged in a previous review.

This non-API library code should use the throwing parse() method per coding guidelines.

🤖 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/aggregations.ts`
around lines 42 - 67, The validateAggregatedReferrerMetricsRevShareLimit
function currently uses makePriceEthSchema(...).safeParse(...) and
makePriceUsdcSchema(...).safeParse(...) with manual error throws; replace these
safeParse + if (!success) blocks by calling the throwing parse() method on the
schemas (e.g.,
makePriceEthSchema("AggregatedReferrerMetricsRevShareLimit.grandTotalRevenueContribution").parse(metrics.grandTotalRevenueContribution)
and similarly for the awardPoolRemaining USDC schema) so Zod will throw on
failure and you can remove the manual parseResultEth/parseResultUsdc checks and
custom Error constructions.
packages/ens-referrals/src/v1/award-models/rev-share-limit/api/zod-schemas.ts (1)

29-47: Hardcoded "rev-share-limit" string literal — already flagged in a previous review.

Line 31 uses a raw string "rev-share-limit" instead of the ReferralProgramAwardModels.RevShareLimit constant used everywhere else in this file.

🤖 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/api/zod-schemas.ts`
around lines 29 - 47, The awardModel literal is hardcoded as "rev-share-limit";
replace it with the constant ReferralProgramAwardModels.RevShareLimit to match
the rest of the file and avoid duplication. Update the z.object({ awardModel:
... }) entry in the zod schema (the block that constructs the rev-share-limit
schema) to use ReferralProgramAwardModels.RevShareLimit instead of the raw
string, keeping the surrounding validation and .refine(...) logic unchanged.
packages/ens-referrals/src/v1/award-models/rev-share-limit/api/serialize.ts (1)

59-92: Awarded and Unranked serializers duplicate field mappings — already flagged in a previous review.

The bodies of serializeAwardedReferrerMetricsRevShareLimit and serializeUnrankedReferrerMetricsRevShareLimit are identical. A shared helper could reduce duplication, though this mirrors the PieSplit pattern for consistency.

🤖 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/api/serialize.ts`
around lines 59 - 92, Both serializers duplicate identical field mappings;
extract the common mapping into a single helper (e.g.,
serializeCommonReferrerMetricsRevShareLimit) that accepts the metrics type and
returns the shared Serialized... object, then have
serializeAwardedReferrerMetricsRevShareLimit and
serializeUnrankedReferrerMetricsRevShareLimit call that helper and return its
result; update references to use the helper and keep specialized serializers
only as thin wrappers to preserve API shape (referencing the functions
serializeAwardedReferrerMetricsRevShareLimit and
serializeUnrankedReferrerMetricsRevShareLimit).
packages/ens-referrals/src/v1/award-models/rev-share-limit/metrics.ts (1)

130-143: Missing runtime validation — already flagged in a previous review.

RevShareLimit metric builders lack runtime validation that PieSplit has (e.g., validateUnrankedReferrerMetricsPieSplit). This was raised in a prior review cycle.

🤖 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/metrics.ts` around
lines 130 - 143, buildUnrankedReferrerMetricsRevShareLimit is missing runtime
validation similar to PieSplit; add a validator (e.g.,
validateUnrankedReferrerMetricsRevShareLimit) that checks the constructed object
shape/values (mirroring validateUnrankedReferrerMetricsPieSplit) and call it
before returning in buildUnrankedReferrerMetricsRevShareLimit so the function
throws or returns an error when invalid data is created; use the existing
buildReferrerMetrics call and the same validation rules used by PieSplit to
implement the new validator and invoke it right after constructing metrics.
packages/ens-referrals/src/v1/award-models/rev-share-limit/leaderboard.ts (1)

50-63: JSDoc copy-paste errors — already flagged in a previous review.

Lines 51, 58, and 61 incorrectly reference AwardedReferrerMetricsPieSplit and a score field that doesn't exist in the RevShareLimit model.

🤖 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/leaderboard.ts`
around lines 50 - 63, Update the JSDoc on the referrers map to remove the
copy-paste mistakes: replace any mentions of AwardedReferrerMetricsPieSplit with
AwardedReferrerMetricsRevShareLimit and remove the incorrect reference to a
nonexistent `score` field; instead ensure the invariants only mention actual
fields present on AwardedReferrerMetricsRevShareLimit (e.g., `totalReferrals`,
`totalIncrementalDuration`, or the correct share field if present) so the
comments accurately describe the referrers: Map<Address,
AwardedReferrerMetricsRevShareLimit> property and its guarantees.
packages/ens-referrals/src/v1/award-models/pie-split/metrics.ts (2)

273-317: 🛠️ Refactor suggestion | 🟠 Major

Prefer zod.parse(...) over safeParse + manual throw.
This is non-API validation and the function throws on failure anyway; using parse(...) is simpler and aligns with the guideline.

♻️ Proposed refactor
   const priceEthSchema = makePriceEthSchema(
     "UnrankedReferrerMetricsPieSplit.totalRevenueContribution",
   );
-  const ethParseResult = priceEthSchema.safeParse(metrics.totalRevenueContribution);
-  if (!ethParseResult.success) {
-    throw new Error(
-      `Invalid UnrankedReferrerMetricsPieSplit: totalRevenueContribution validation failed: ${ethParseResult.error.message}`,
-    );
-  }
+  priceEthSchema.parse(metrics.totalRevenueContribution);
...
   const priceUsdcSchema = makePriceUsdcSchema(
     "UnrankedReferrerMetricsPieSplit.awardPoolApproxValue",
   );
-  const usdcParseResult = priceUsdcSchema.safeParse(metrics.awardPoolApproxValue);
-  if (!usdcParseResult.success) {
-    throw new Error(
-      `Invalid UnrankedReferrerMetricsPieSplit: awardPoolApproxValue validation failed: ${usdcParseResult.error.message}`,
-    );
-  }
+  priceUsdcSchema.parse(metrics.awardPoolApproxValue);

As per coding guidelines, "Use zod.parse(...) for non-API code (config, SDK, scripts) when invalid input should throw immediately; use zod.safeParse(...) when you need a non-throwing branch".

🤖 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/pie-split/metrics.ts` around lines
273 - 317, Replace the safeParse + manual throw pattern with zod.parse to
simplify validation: use priceEthSchema.parse(metrics.totalRevenueContribution)
instead of priceEthSchema.safeParse(...) and removing the ethParseResult
check/throw, and likewise use
priceUsdcSchema.parse(metrics.awardPoolApproxValue) instead of
priceUsdcSchema.safeParse(...) and remove the usdcParseResult check/throw; keep
the subsequent explicit domain checks (the
amount/score/finalScore/awardPoolShare zero checks) intact so only the schema
parsing is simplified.

168-178: ⚠️ Potential issue | 🟡 Minor

Fix stale JSDoc type reference.
AggregatedRankedReferrerMetricsPieSplit doesn’t exist here; the imported type is AggregatedReferrerMetricsPieSplit. Update the links to prevent broken docs.

📝 Proposed fix
- * relative to {`@link` AggregatedRankedReferrerMetricsPieSplit}.
+ * relative to {`@link` AggregatedReferrerMetricsPieSplit}.
...
- * `@invariant` Calculated as: `finalScore / {`@link` AggregatedRankedReferrerMetricsPieSplit.grandTotalQualifiedReferrersFinalScore}` if `isQualified` is `true`, else `0`
+ * `@invariant` Calculated as: `finalScore / {`@link` AggregatedReferrerMetricsPieSplit.grandTotalQualifiedReferrersFinalScore}` if `isQualified` is `true`, else `0`
🤖 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/pie-split/metrics.ts` around lines
168 - 178, Update the stale JSDoc type links in the
AwardedReferrerMetricsPieSplit interface: replace references to
AggregatedRankedReferrerMetricsPieSplit with the correct imported type
AggregatedReferrerMetricsPieSplit in the top description and in the `@invariant`
line so the docs link to the actual exported type; edit the JSDoc around
AwardedReferrerMetricsPieSplit / RankedReferrerMetricsPieSplit to use
AggregatedReferrerMetricsPieSplit consistently.
packages/ens-referrals/src/v1/api/deserialize.ts (1)

75-78: ⚠️ Potential issue | 🟠 Major

Unsafe casts hide schema/type mismatch for unknown award models.
The passthrough schema widens output to { awardModel: string } & Record<string, unknown>, but the public types don’t include an unknown variant. Casting to the narrow types can yield runtime mismatches (e.g., serializers or callers expecting known shapes). Consider either (a) adding explicit “unknown” variants to public types + serializer handling, or (b) removing passthrough so unknown award models are rejected, and then drop these casts.

Also applies to: 97-98

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ens-referrals/src/v1/api/deserialize.ts` around lines 75 - 78, The
current unsafe cast in the return of deserialize (the cast "as unknown as
ReferralProgramEditionConfig[]") hides schema/type mismatches caused by using
.passthrough() in makeReferralProgramRulesSchema; fix by either (A) modelling
and handling unknown award models explicitly: add a new variant type (e.g.,
ReferralProgramEditionConfigUnknown { awardModel: string } & Record<string,
unknown>), update serializers/deserializers to return a union
(ReferralProgramEditionConfig | ReferralProgramEditionConfigUnknown) and change
the return sites in deserialize (both the current return and the other one
around the 97-98 area) to return that union without unsafe casts, or (B) remove
.passthrough() from makeReferralProgramRulesSchema so unknown award models are
rejected by validation and then remove the unsafe casts and keep the return type
as ReferralProgramEditionConfig[]; apply the same change at both affected return
points and adjust callers to handle the new union or validation errors.
apps/ensapi/src/lib/ensanalytics/referrer-leaderboard/get-referrer-leaderboard-v1.test.ts (1)

72-75: ⚠️ Potential issue | 🟠 Major

Fix PieSplit finalScore assertion.
PieSplit final score uses the multiplier 1 + finalScoreBoost.

Suggested fix
-        qualifiedReferrers.every(([_, r]) => r.finalScore === r.score * r.finalScoreBoost),
+        qualifiedReferrers.every(([_, r]) => r.finalScore === r.score * (1 + r.finalScoreBoost)),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/ensapi/src/lib/ensanalytics/referrer-leaderboard/get-referrer-leaderboard-v1.test.ts`
around lines 72 - 75, The test assertion for PieSplit finalScore is incorrect:
update the assertion that checks qualifiedReferrers so it expects finalScore to
equal score multiplied by (1 + finalScoreBoost) instead of score *
finalScoreBoost; specifically change the predicate using qualifiedReferrers (the
tuple [_, r] with r.score, r.finalScoreBoost, r.finalScore) to assert
r.finalScore === r.score * (1 + r.finalScoreBoost).
🤖 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/pie-split/metrics.ts`:
- Around line 20-88: The interfaces currently document invariants inline on
ScoredReferrerMetricsPieSplit.score and
RankedReferrerMetricsPieSplit.finalScoreBoost/finalScore; extract those
invariant-bearing fields into dedicated type aliases (e.g.,
ReferrerScoreWithInvariant, FinalScoreBoost (0..1), FinalReferrerScore) and
replace the inline property types with those aliases in
ScoredReferrerMetricsPieSplit and RankedReferrerMetricsPieSplit, update any
references to calcReferrerScorePieSplit, validateReferrerScore and validate
functions to accept/return the new aliases, and move the invariant JSDoc
comments onto the new type aliases so each invariant is documented exactly once.

In `@packages/ens-referrals/src/v1/award-models/rev-share-limit/leaderboard.ts`:
- Around line 136-155: The code currently accumulates per-event truncated base
revenue into state.totalBaseRevenueContributionAmount (via
incrementalBaseRevenueAmount computed from BASE_REVENUE_CONTRIBUTION_PER_YEAR
and event.incrementalDuration) but final metric builders
(buildReferrerMetricsRevShareLimit / buildRankedReferrerMetricsRevShareLimit and
isReferrerQualifiedRevShareLimit) recompute floor(BASE × Σduration /
SECONDS_PER_YEAR), causing possible off-by-one discrepancies; to fix, make the
final metric builders use the race-accumulated value
(state.totalBaseRevenueContributionAmount) and derived
accumulatedStandardAwardAmount instead of re-aggregating durations—update
buildReferrerMetricsRevShareLimit and isReferrerQualifiedRevShareLimit to
accept/consume the accumulated totalBaseRevenueContributionAmount and
accumulatedStandardAwardAmount (or pass them through
buildRankedReferrerMetricsRevShareLimit) so qualification and
awardPoolApproxValue are computed from the same per-event-truncated totals as
used in the race logic (references: incrementalBaseRevenueAmount,
BASE_REVENUE_CONTRIBUTION_PER_YEAR, totalBaseRevenueContributionAmount,
accumulatedStandardAwardAmount, buildReferrerMetricsRevShareLimit,
buildRankedReferrerMetricsRevShareLimit, isReferrerQualifiedRevShareLimit).
- Around line 100-112: The current deterministic sorting in
buildReferrerLeaderboardRevShareLimit is redundant for callers that already call
getReferralEvents (which returns rows ordered by timestamp, blockNumber,
transactionHash) and imposes an unnecessary O(n log n) cost; add an optional
boolean parameter (e.g., presorted or assumeSorted) to
buildReferrerLeaderboardRevShareLimit and, when true, skip the defensive sort so
callers that use getReferralEvents can pass presorted=true to avoid the sort,
otherwise keep the existing sort behavior for safety; update the function
signature and the conditional around the sorting block accordingly and document
the new parameter so callers know to pass presorted when they rely on
getReferralEvents ordering.
- Around line 183-209: The comparator for sortedAddresses recomputes
standardAwardValue via scalePrice(priceUsdc(...), rules.qualifiedRevenueShare)
on every comparison; precompute those values once into a Map (keyed by referrer
address) before calling sort, e.g. iterate referrerStates to compute
standardAmounts.get(addr) using
priceUsdc(state.totalBaseRevenueContributionAmount) and scalePrice(...).amount,
then update the comparator to read from that Map (and still use
referrerStates.get(...) for qualifiedAwardValueAmount and the referrer
lexicographic tie-breaker) so scalePrice/priceUsdc are not invoked inside the
compare function.

---

Outside diff comments:
In
`@apps/ensapi/src/lib/ensanalytics/referrer-leaderboard/get-referrer-leaderboard-v1.test.ts`:
- Around line 48-51: The test currently calls result.referrers.entries() which
returns a single-use iterator and then calls take()/drop() and multiple .every()
assertions against it; materialize the iterator into an array first to avoid
vacuous/consumed iterations — e.g., assign const referrerArray =
Array.from(result.referrers.entries()), then derive qualifiedReferrers and
unqualifiedReferrers by slicing with rules.maxQualifiedReferrers (e.g.,
referrerArray.slice(0, rules.maxQualifiedReferrers) and
referrerArray.slice(rules.maxQualifiedReferrers)), and use those arrays for all
subsequent assertions (references: referrers, qualifiedReferrers,
unqualifiedReferrers, rules.maxQualifiedReferrers).

---

Duplicate comments:
In
`@apps/ensapi/src/lib/ensanalytics/referrer-leaderboard/get-referrer-leaderboard-v1.test.ts`:
- Around line 72-75: The test assertion for PieSplit finalScore is incorrect:
update the assertion that checks qualifiedReferrers so it expects finalScore to
equal score multiplied by (1 + finalScoreBoost) instead of score *
finalScoreBoost; specifically change the predicate using qualifiedReferrers (the
tuple [_, r] with r.score, r.finalScoreBoost, r.finalScore) to assert
r.finalScore === r.score * (1 + r.finalScoreBoost).

In `@packages/ens-referrals/src/v1/api/deserialize.ts`:
- Around line 75-78: The current unsafe cast in the return of deserialize (the
cast "as unknown as ReferralProgramEditionConfig[]") hides schema/type
mismatches caused by using .passthrough() in makeReferralProgramRulesSchema; fix
by either (A) modelling and handling unknown award models explicitly: add a new
variant type (e.g., ReferralProgramEditionConfigUnknown { awardModel: string } &
Record<string, unknown>), update serializers/deserializers to return a union
(ReferralProgramEditionConfig | ReferralProgramEditionConfigUnknown) and change
the return sites in deserialize (both the current return and the other one
around the 97-98 area) to return that union without unsafe casts, or (B) remove
.passthrough() from makeReferralProgramRulesSchema so unknown award models are
rejected by validation and then remove the unsafe casts and keep the return type
as ReferralProgramEditionConfig[]; apply the same change at both affected return
points and adjust callers to handle the new union or validation errors.

In `@packages/ens-referrals/src/v1/award-models/pie-split/metrics.ts`:
- Around line 273-317: Replace the safeParse + manual throw pattern with
zod.parse to simplify validation: use
priceEthSchema.parse(metrics.totalRevenueContribution) instead of
priceEthSchema.safeParse(...) and removing the ethParseResult check/throw, and
likewise use priceUsdcSchema.parse(metrics.awardPoolApproxValue) instead of
priceUsdcSchema.safeParse(...) and remove the usdcParseResult check/throw; keep
the subsequent explicit domain checks (the
amount/score/finalScore/awardPoolShare zero checks) intact so only the schema
parsing is simplified.
- Around line 168-178: Update the stale JSDoc type links in the
AwardedReferrerMetricsPieSplit interface: replace references to
AggregatedRankedReferrerMetricsPieSplit with the correct imported type
AggregatedReferrerMetricsPieSplit in the top description and in the `@invariant`
line so the docs link to the actual exported type; edit the JSDoc around
AwardedReferrerMetricsPieSplit / RankedReferrerMetricsPieSplit to use
AggregatedReferrerMetricsPieSplit consistently.

In `@packages/ens-referrals/src/v1/award-models/rev-share-limit/aggregations.ts`:
- Around line 42-67: The validateAggregatedReferrerMetricsRevShareLimit function
currently uses makePriceEthSchema(...).safeParse(...) and
makePriceUsdcSchema(...).safeParse(...) with manual error throws; replace these
safeParse + if (!success) blocks by calling the throwing parse() method on the
schemas (e.g.,
makePriceEthSchema("AggregatedReferrerMetricsRevShareLimit.grandTotalRevenueContribution").parse(metrics.grandTotalRevenueContribution)
and similarly for the awardPoolRemaining USDC schema) so Zod will throw on
failure and you can remove the manual parseResultEth/parseResultUsdc checks and
custom Error constructions.

In `@packages/ens-referrals/src/v1/award-models/rev-share-limit/api/serialize.ts`:
- Around line 59-92: Both serializers duplicate identical field mappings;
extract the common mapping into a single helper (e.g.,
serializeCommonReferrerMetricsRevShareLimit) that accepts the metrics type and
returns the shared Serialized... object, then have
serializeAwardedReferrerMetricsRevShareLimit and
serializeUnrankedReferrerMetricsRevShareLimit call that helper and return its
result; update references to use the helper and keep specialized serializers
only as thin wrappers to preserve API shape (referencing the functions
serializeAwardedReferrerMetricsRevShareLimit and
serializeUnrankedReferrerMetricsRevShareLimit).

In
`@packages/ens-referrals/src/v1/award-models/rev-share-limit/api/zod-schemas.ts`:
- Around line 29-47: The awardModel literal is hardcoded as "rev-share-limit";
replace it with the constant ReferralProgramAwardModels.RevShareLimit to match
the rest of the file and avoid duplication. Update the z.object({ awardModel:
... }) entry in the zod schema (the block that constructs the rev-share-limit
schema) to use ReferralProgramAwardModels.RevShareLimit instead of the raw
string, keeping the surrounding validation and .refine(...) logic unchanged.

In `@packages/ens-referrals/src/v1/award-models/rev-share-limit/leaderboard.ts`:
- Around line 50-63: Update the JSDoc on the referrers map to remove the
copy-paste mistakes: replace any mentions of AwardedReferrerMetricsPieSplit with
AwardedReferrerMetricsRevShareLimit and remove the incorrect reference to a
nonexistent `score` field; instead ensure the invariants only mention actual
fields present on AwardedReferrerMetricsRevShareLimit (e.g., `totalReferrals`,
`totalIncrementalDuration`, or the correct share field if present) so the
comments accurately describe the referrers: Map<Address,
AwardedReferrerMetricsRevShareLimit> property and its guarantees.

In `@packages/ens-referrals/src/v1/award-models/rev-share-limit/metrics.ts`:
- Around line 130-143: buildUnrankedReferrerMetricsRevShareLimit is missing
runtime validation similar to PieSplit; add a validator (e.g.,
validateUnrankedReferrerMetricsRevShareLimit) that checks the constructed object
shape/values (mirroring validateUnrankedReferrerMetricsPieSplit) and call it
before returning in buildUnrankedReferrerMetricsRevShareLimit so the function
throws or returns an error when invalid data is created; use the existing
buildReferrerMetrics call and the same validation rules used by PieSplit to
implement the new validator and invoke it right after constructing metrics.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4829b18 and 85f0e51.

📒 Files selected for processing (18)
  • apps/ensapi/src/handlers/ensanalytics-api-v1.test.ts
  • apps/ensapi/src/lib/ensanalytics/referrer-leaderboard/database-v1.ts
  • apps/ensapi/src/lib/ensanalytics/referrer-leaderboard/get-referrer-leaderboard-v1.test.ts
  • apps/ensapi/src/lib/ensanalytics/referrer-leaderboard/get-referrer-leaderboard-v1.ts
  • packages/ens-referrals/src/v1/api/deserialize.ts
  • packages/ens-referrals/src/v1/api/zod-schemas.ts
  • packages/ens-referrals/src/v1/award-models/pie-split/metrics.ts
  • packages/ens-referrals/src/v1/award-models/rev-share-limit/aggregations.ts
  • packages/ens-referrals/src/v1/award-models/rev-share-limit/api/serialize.ts
  • packages/ens-referrals/src/v1/award-models/rev-share-limit/api/serialized-types.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/referral-event.ts
  • packages/ens-referrals/src/v1/client.ts
  • packages/ens-referrals/src/v1/index.ts
  • packages/ens-referrals/src/v1/leaderboard.ts

Copilot AI review requested due to automatic review settings February 24, 2026 03:17
@vercel vercel bot temporarily deployed to Preview – admin.ensnode.io February 24, 2026 03:17 Inactive
@vercel vercel bot temporarily deployed to Preview – ensrainbow.io February 24, 2026 03:17 Inactive
@vercel vercel bot temporarily deployed to Preview – ensnode.io February 24, 2026 03:17 Inactive
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 51 out of 51 changed files in this pull request and generated 4 comments.


💡 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: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/ensapi/src/lib/ensanalytics/referrer-leaderboard/get-referrer-leaderboard.test.ts (1)

43-46: ⚠️ Potential issue | 🟠 Major

Pre-existing iterator sharing bug makes the unqualified referrer assertions vacuously true.

qualifiedReferrers and unqualifiedReferrers both wrap the same referrers MapIterator. Each iterator helper method like .take() and .drop() returns a new iterator, but .every() is an eagerly-consuming terminal operation. The execution order matters:

  1. qualifiedReferrers.every(...) (line 52) consumes the first n elements from the shared referrers iterator.
  2. unqualifiedReferrers.drop(n) then tries to skip another n from where referrers already sits (at position n), yielding only elements [2n..end].

If the mock dataset has fewer than 2 × maxQualifiedReferrers entries total, unqualifiedReferrers will be empty and every .every() call on it passes vacuously — the unqualified assertions are never actually exercised.

Fix: create the unqualifiedReferrers view from a fresh call to result.referrers.entries(), or collect both partitions in a single pass:

🐛 Proposed fix
-      const referrers = result.referrers.entries();
-      const qualifiedReferrers = referrers.take(rules.maxQualifiedReferrers);
-      const unqualifiedReferrers = referrers.drop(rules.maxQualifiedReferrers);
+      const qualifiedReferrers = result.referrers.entries().take(rules.maxQualifiedReferrers);
+      const unqualifiedReferrers = result.referrers.entries().drop(rules.maxQualifiedReferrers);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/ensapi/src/lib/ensanalytics/referrer-leaderboard/get-referrer-leaderboard.test.ts`
around lines 43 - 46, The test wrongly reuses the same Map iterator (referrers =
result.referrers.entries()) for both qualifiedReferrers and unqualifiedReferrers
so .take() consumes items before .drop() runs, making unqualified assertions
vacuously true; fix by creating a fresh iterator for the unqualified partition
(call result.referrers.entries() again before applying .drop()) or alternatively
materialize the entries into an array (Array.from(result.referrers.entries()))
and then compute qualified via .slice(0, rules.maxQualifiedReferrers) and
unqualified via .slice(rules.maxQualifiedReferrers) so .every() on each
partition actually inspects the intended elements (update references to
referrers, qualifiedReferrers, and unqualifiedReferrers accordingly).
♻️ Duplicate comments (3)
packages/ens-referrals/src/v1/award-models/pie-split/metrics.ts (1)

273-317: 🧹 Nitpick | 🔵 Trivial

Prefer zod.parse(...) over safeParse + manual throw.

Lines 276 and 312 use safeParse followed by a manual throw on failure. Since this is non-API validation code and invalid input should throw immediately, parse(...) is more concise and idiomatic.

Proposed fix (for both occurrences)
-  const priceEthSchema = makePriceEthSchema(
-    "UnrankedReferrerMetricsPieSplit.totalRevenueContribution",
-  );
-  const ethParseResult = priceEthSchema.safeParse(metrics.totalRevenueContribution);
-  if (!ethParseResult.success) {
-    throw new Error(
-      `Invalid UnrankedReferrerMetricsPieSplit: totalRevenueContribution validation failed: ${ethParseResult.error.message}`,
-    );
-  }
+  makePriceEthSchema(
+    "UnrankedReferrerMetricsPieSplit.totalRevenueContribution",
+  ).parse(metrics.totalRevenueContribution);
-  const priceUsdcSchema = makePriceUsdcSchema(
-    "UnrankedReferrerMetricsPieSplit.awardPoolApproxValue",
-  );
-  const usdcParseResult = priceUsdcSchema.safeParse(metrics.awardPoolApproxValue);
-  if (!usdcParseResult.success) {
-    throw new Error(
-      `Invalid UnrankedReferrerMetricsPieSplit: awardPoolApproxValue validation failed: ${usdcParseResult.error.message}`,
-    );
-  }
+  makePriceUsdcSchema(
+    "UnrankedReferrerMetricsPieSplit.awardPoolApproxValue",
+  ).parse(metrics.awardPoolApproxValue);

As per coding guidelines, "Use zod.parse(...) for non-API code (config, SDK, scripts) when invalid input should throw immediately."

🤖 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/pie-split/metrics.ts` around lines
273 - 317, Replace the safeParse + manual throw pattern with zod.parse for the
two schema validations: call
priceEthSchema.parse(metrics.totalRevenueContribution) instead of
priceEthSchema.safeParse(...) and remove ethParseResult and its failure branch;
likewise call priceUsdcSchema.parse(metrics.awardPoolApproxValue) instead of
priceUsdcSchema.safeParse(...) and remove usdcParseResult and its failure
branch. Keep the subsequent zero-check assertions
(metrics.totalRevenueContribution.amount, metrics.score, finalScoreBoost,
finalScore, awardPoolShare) intact; only change the validation lines that create
priceEthSchema and priceUsdcSchema and how they are applied.
packages/ens-referrals/src/v1/award-models/rev-share-limit/rules.ts (1)

53-87: 🛠️ Refactor suggestion | 🟠 Major

Prefer parse() over safeParse + manual throw here.

Line 54, Line 64, and Line 82 use safeParse then throw; in non-API validation, parse() keeps the throwing behavior and removes boilerplate.

♻️ Suggested simplification
-  const poolResult = poolSchema.safeParse(rules.totalAwardPoolValue);
-  if (!poolResult.success) {
-    throw new Error(
-      `ReferralProgramRulesRevShareLimit: totalAwardPoolValue validation failed: ${poolResult.error.message}`,
-    );
-  }
+  poolSchema.parse(rules.totalAwardPoolValue);

   const minSchema = makePriceUsdcSchema(
     "ReferralProgramRulesRevShareLimit.minQualifiedRevenueContribution",
   );
-  const minResult = minSchema.safeParse(rules.minQualifiedRevenueContribution);
-  if (!minResult.success) {
-    throw new Error(
-      `ReferralProgramRulesRevShareLimit: minQualifiedRevenueContribution validation failed: ${minResult.error.message}`,
-    );
-  }
+  minSchema.parse(rules.minQualifiedRevenueContribution);

   const accountIdSchema = makeAccountIdSchema("ReferralProgramRulesRevShareLimit.subregistryId");
-  const accountIdResult = accountIdSchema.safeParse(rules.subregistryId);
-  if (!accountIdResult.success) {
-    throw new Error(
-      `ReferralProgramRulesRevShareLimit: subregistryId validation failed: ${accountIdResult.error.message}`,
-    );
-  }
+  accountIdSchema.parse(rules.subregistryId);

As per coding guidelines: Use zod.parse(...) for non-API code (config, SDK, scripts) when invalid input should throw immediately; use zod.safeParse(...) when you need a non-throwing branch.

🤖 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 53 - 87, Replace the safeParse + manual throw boilerplate by calling
zod.parse() directly for the three schema validations: use
poolSchema.parse(rules.totalAwardPoolValue) instead of poolSchema.safeParse(...)
and the subsequent if/throw, use
minSchema.parse(rules.minQualifiedRevenueContribution) instead of
minSchema.safeParse(...) and remove its if/throw, and use
accountIdSchema.parse(rules.subregistryId) instead of
accountIdSchema.safeParse(...) and remove that if/throw; keep the existing
qualifiedRevenueShare numeric-range check as-is. Ensure you still reference the
same schema factories (makePriceUsdcSchema and makeAccountIdSchema) and the same
contextual names (ReferralProgramRulesRevShareLimit.*) so errors thrown include
the schema path.
packages/ens-referrals/src/v1/award-models/rev-share-limit/leaderboard.ts (1)

135-151: ⚠️ Potential issue | 🟡 Minor

Per-event truncation can diverge from aggregate recomputation.

Line 135-151 accumulates per-event truncated base revenue, but Line 225-236 recomputes base revenue from totalIncrementalDuration. These floors can differ, leading to small inconsistencies between pool-claim qualification and final standardAwardValue/isQualified. Consider building final metrics from the race-accumulated totals to keep them aligned.

🧩 Possible alignment
-      const revShareMetrics = buildReferrerMetricsRevShareLimit(baseMetrics);
+      const revShareMetrics = {
+        ...buildReferrerMetricsRevShareLimit(baseMetrics),
+        totalBaseRevenueContribution: priceUsdc(state.totalBaseRevenueContributionAmount),
+      };

-      const standardAwardValue = scalePrice(
-        revShareMetrics.totalBaseRevenueContribution,
-        rules.qualifiedRevenueShare,
-      );
+      const standardAwardValue = priceUsdc(state.accumulatedStandardAwardAmount);

Also applies to: 225-236

🤖 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/leaderboard.ts`
around lines 135 - 151, The code currently adds per-event truncated values
(incrementalBaseRevenueAmount and incrementalStandardAwardAmount) into state,
causing divergence from later recomputation; instead stop accumulating those
per-event truncated amounts and only accumulate raw totals
(state.totalReferrals, state.totalIncrementalDuration,
state.totalRevenueContributionAmount using
event.incrementalRevenueContribution.amount). Remove updating
state.totalBaseRevenueContributionAmount and
state.accumulatedStandardAwardAmount here and ensure final
totalBaseRevenueContributionAmount and accumulatedStandardAwardAmount (used to
derive standardAwardValue/isQualified) are computed once from the aggregated
totals using the same formulas/constants (BASE_REVENUE_CONTRIBUTION_PER_YEAR,
SECONDS_PER_YEAR, priceUsdc, scalePrice, rules.qualifiedRevenueShare) as in the
later block so truncation happens only at final aggregation.
🤖 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/pie-split/metrics.ts`:
- Around line 190-209: The validator currently only bounds-checks awardPoolShare
and awardPoolApproxValue; update validateAwardedReferrerMetricsPieSplit to also
assert the documented invariant by recomputing the expected awardPoolShare from
awardPoolApproxValue and rules.totalAwardPoolValue and comparing it to
referrer.awardPoolShare (use a safe comparison: convert the BigInt amounts to
Number and compare with a small epsilon, or compare cross-multiplied BigInts to
avoid floating errors). Locate this logic inside
validateAwardedReferrerMetricsPieSplit (and reuse any helper patterns from
validateRankedReferrerMetricsPieSplit) and throw a descriptive Error if the
recomputed share and referrer.awardPoolShare differ beyond the allowed
tolerance.
- Around line 154-166: The calcReferrerAwardPoolSharePieSplit function
recalculates a referrer's final score instead of using the already-computed
value on the referrer object; update calcReferrerAwardPoolSharePieSplit to
return 0 when unqualified or when
aggregatedMetrics.grandTotalQualifiedReferrersFinalScore is 0, otherwise divide
referrer.finalScore by aggregatedMetrics.grandTotalQualifiedReferrersFinalScore
(remove the call to calcReferrerFinalScorePieSplit(referrer.rank,
referrer.totalIncrementalDuration, rules) and use referrer.finalScore instead)
so the function uses the validated stored score.

In `@packages/ens-referrals/src/v1/award-models/pie-split/score.ts`:
- Line 15: Remove the redundant JSDoc `@returns` tag in the JSDoc block that says
"@returns The score of the referrer." in
packages/ens-referrals/src/v1/award-models/pie-split/score.ts — locate the JSDoc
immediately above the function that computes/returns the referrer score (the
exported score/get‑referrer‑score function) and delete that `@returns` line so the
doc no longer restates the summary.

In
`@packages/ens-referrals/src/v1/award-models/rev-share-limit/edition-metrics.ts`:
- Around line 67-118: The interface-level invariant for awardModel is already
documented on ReferrerEditionMetricsUnrankedRevShareLimit and
ReferrerEditionMetricsRankedRevShareLimit, so remove the redundant
property-level `@invariant` JSDoc on each interface's awardModel property: delete
the inline `@invariant Always equals 'rules.awardModel'
(ReferralProgramAwardModels.RevShareLimit)` tag from the awardModel property in
ReferrerEditionMetricsUnrankedRevShareLimit and likewise remove the duplicate
awardModel property-level `@invariant` in
ReferrerEditionMetricsRankedRevShareLimit, leaving the single invariant
documented on the interface JSDoc only.
- Around line 13-65: Remove the redundant `@invariant` JSDoc tag from the
awardModel property in the ReferrerEditionMetricsRankedRevShareLimit interface
(the invariant is already declared in the type-level JSDoc) and do the same for
the awardModel property in ReferrerEditionMetricsUnrankedRevShareLimit; leave
the type-level JSDoc unchanged and only remove the per-property `@invariant`
annotations to consolidate the invariant documentation to the type.

In `@packages/ens-referrals/src/v1/award-models/rev-share-limit/leaderboard.ts`:
- Around line 29-69: Refactor ReferrerLeaderboardRevShareLimit by extracting
type aliases for the two properties that currently carry `@invariant` JSDoc:
create a type alias (e.g., ReferrerLeaderboardAwardModelRevShareLimit) for
awardModel that documents the invariant "Always equals rules.awardModel
(ReferralProgramAwardModels.RevShareLimit)" and update the interface to use that
alias for awardModel, and create a type alias (e.g.,
ReferrerLeaderboardReferrersMap) for referrers that carries the
ordering/emptiness/non-zero-value invariants (referencing Map<Address,
AwardedReferrerMetricsRevShareLimit>) and replace the referrers property with
that alias; keep the rest of ReferrerLeaderboardRevShareLimit unchanged and
ensure JSDoc invariants are removed from the interface properties and placed on
the new type aliases.

In `@packages/ens-referrals/src/v1/award-models/rev-share-limit/metrics.ts`:
- Around line 74-96: Create type aliases that carry the invariants and then use
them in the interfaces: define aliases for totalBaseRevenueContribution (e.g.,
TotalBaseRevenueContribution = PriceUsdc with its invariant doc), isQualified
(e.g., IsQualified = boolean with its invariant doc), standardAwardValue (e.g.,
StandardAwardValue = PriceUsdc with its invariant doc), and awardPoolApproxValue
(e.g., AwardPoolApproxValue = PriceUsdc with its invariant doc); replace the
direct property types in RankedReferrerMetricsRevShareLimit and
AwardedReferrerMetricsRevShareLimit to use these aliases and remove the
duplicated `@invariant` tags from the interfaces so each invariant is documented
once on the corresponding type alias.
- Around line 18-26: Convert the three exported interfaces
ReferrerMetricsRevShareLimit, RankedReferrerMetricsRevShareLimit, and
AwardedReferrerMetricsRevShareLimit into exported type aliases (keeping their
exact names and shapes) and move each `@invariant` JSDoc from the interface-level
comment to the type-alias-level comment above the new type alias; update any
existing references/imports to these types if necessary but do not change
property names or visibility (e.g., totalBaseRevenueContribution, rank, awarded
fields) — just replace the "interface" declarations with "export type ... = {
... }" and place the invariant block immediately above each corresponding type
alias.
- Around line 45-57: Convert the interface RankedReferrerMetricsRevShareLimit
into a type alias that extends ReferrerMetricsRevShareLimit and retains the
rank: ReferrerRank and isQualified: boolean properties; move the `@invariant`
comment that currently sits on the isQualified property up to the type alias
declaration and document it there as: "true iff totalBaseRevenueContribution >=
ReferralProgramRulesRevShareLimit.minQualifiedRevenueContribution", and keep the
existing doc comment for rank referencing ReferrerLeaderboardRevShareLimit;
ensure no other property-level invariants remain.

In `@packages/ens-referrals/src/v1/award-models/rev-share-limit/rules.ts`:
- Around line 20-48: Convert the interface ReferralProgramRulesRevShareLimit
into a type alias that intersects BaseReferralProgramRules (e.g. type
ReferralProgramRulesRevShareLimit = BaseReferralProgramRules & { ... }) and move
the invariant JSDoc about qualifiedRevenueShare (the guarantee that it's a
number between 0 and 1 inclusive) from the property-level comment into the
type-alias level JSDoc above the new alias; keep the same properties
(awardModel, totalAwardPoolValue, minQualifiedRevenueContribution,
qualifiedRevenueShare) and preserve the discriminant value
ReferralProgramAwardModels.RevShareLimit and existing types like PriceUsdc.

---

Outside diff comments:
In
`@apps/ensapi/src/lib/ensanalytics/referrer-leaderboard/get-referrer-leaderboard.test.ts`:
- Around line 43-46: The test wrongly reuses the same Map iterator (referrers =
result.referrers.entries()) for both qualifiedReferrers and unqualifiedReferrers
so .take() consumes items before .drop() runs, making unqualified assertions
vacuously true; fix by creating a fresh iterator for the unqualified partition
(call result.referrers.entries() again before applying .drop()) or alternatively
materialize the entries into an array (Array.from(result.referrers.entries()))
and then compute qualified via .slice(0, rules.maxQualifiedReferrers) and
unqualified via .slice(rules.maxQualifiedReferrers) so .every() on each
partition actually inspects the intended elements (update references to
referrers, qualifiedReferrers, and unqualifiedReferrers accordingly).

---

Duplicate comments:
In `@packages/ens-referrals/src/v1/award-models/pie-split/metrics.ts`:
- Around line 273-317: Replace the safeParse + manual throw pattern with
zod.parse for the two schema validations: call
priceEthSchema.parse(metrics.totalRevenueContribution) instead of
priceEthSchema.safeParse(...) and remove ethParseResult and its failure branch;
likewise call priceUsdcSchema.parse(metrics.awardPoolApproxValue) instead of
priceUsdcSchema.safeParse(...) and remove usdcParseResult and its failure
branch. Keep the subsequent zero-check assertions
(metrics.totalRevenueContribution.amount, metrics.score, finalScoreBoost,
finalScore, awardPoolShare) intact; only change the validation lines that create
priceEthSchema and priceUsdcSchema and how they are applied.

In `@packages/ens-referrals/src/v1/award-models/rev-share-limit/leaderboard.ts`:
- Around line 135-151: The code currently adds per-event truncated values
(incrementalBaseRevenueAmount and incrementalStandardAwardAmount) into state,
causing divergence from later recomputation; instead stop accumulating those
per-event truncated amounts and only accumulate raw totals
(state.totalReferrals, state.totalIncrementalDuration,
state.totalRevenueContributionAmount using
event.incrementalRevenueContribution.amount). Remove updating
state.totalBaseRevenueContributionAmount and
state.accumulatedStandardAwardAmount here and ensure final
totalBaseRevenueContributionAmount and accumulatedStandardAwardAmount (used to
derive standardAwardValue/isQualified) are computed once from the aggregated
totals using the same formulas/constants (BASE_REVENUE_CONTRIBUTION_PER_YEAR,
SECONDS_PER_YEAR, priceUsdc, scalePrice, rules.qualifiedRevenueShare) as in the
later block so truncation happens only at final aggregation.

In `@packages/ens-referrals/src/v1/award-models/rev-share-limit/rules.ts`:
- Around line 53-87: Replace the safeParse + manual throw boilerplate by calling
zod.parse() directly for the three schema validations: use
poolSchema.parse(rules.totalAwardPoolValue) instead of poolSchema.safeParse(...)
and the subsequent if/throw, use
minSchema.parse(rules.minQualifiedRevenueContribution) instead of
minSchema.safeParse(...) and remove its if/throw, and use
accountIdSchema.parse(rules.subregistryId) instead of
accountIdSchema.safeParse(...) and remove that if/throw; keep the existing
qualifiedRevenueShare numeric-range check as-is. Ensure you still reference the
same schema factories (makePriceUsdcSchema and makeAccountIdSchema) and the same
contextual names (ReferralProgramRulesRevShareLimit.*) so errors thrown include
the schema path.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 85f0e51 and 5e5cdc6.

📒 Files selected for processing (8)
  • apps/ensapi/src/lib/ensanalytics/referrer-leaderboard/get-referrer-leaderboard.test.ts
  • packages/ens-referrals/src/v1/award-models/pie-split/metrics.ts
  • packages/ens-referrals/src/v1/award-models/pie-split/score.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/edition-metrics.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

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 52 out of 52 changed files in this pull request and generated 5 comments.


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

@Goader Goader marked this pull request as ready for review February 26, 2026 05:02
@Goader Goader requested a review from a team as a code owner February 26, 2026 05:02
@greptile-apps
Copy link
Contributor

greptile-apps bot commented Feb 26, 2026

Greptile Summary

This PR refactors the ens-referrals package to support multiple award models for different referral program editions by introducing a discriminated union architecture.

Key Changes

Architecture Refactoring:

  • Transformed ReferralProgramRules from a single interface into a discriminated union of award model types (pie-split, rev-share-limit, unrecognized)
  • Restructured package with new award-models/ directory containing model-specific implementations
  • Original ENS Holiday Awards logic preserved as pie-split model
  • Introduced ReferralProgramRulesUnrecognized type for client-side forward compatibility

New Rev-Share-Limit Award Model:

  • Implements sequential "race" algorithm where referrers claim awards from a pool on first-come, first-served basis
  • Events processed in deterministic order (timestamp → blockNumber → transactionHash → id)
  • Qualification based on base revenue contribution ($5 per year of duration)
  • Pool claims capped by remaining pool amount after each qualifying event
  • Comprehensive test coverage with 451 lines of tests covering edge cases

Forward Compatibility:

  • Zod schemas preserve unrecognized award models during parsing rather than dropping them
  • Server cache filters out unrecognized editions to prevent runtime errors
  • Client code can handle future award models gracefully without crashing

Database Integration:

  • pie-split uses aggregated GROUP BY query (existing getReferrerMetrics)
  • rev-share-limit uses new getReferralEvents query returning individual rows for sequential processing
  • Dispatch logic in getReferrerLeaderboard routes to appropriate builder based on awardModel discriminant

API Serialization:

  • Refactored to dispatch based on award model type
  • Throws error if attempting to serialize unrecognized editions (prevented at cache level)
  • Exhaustive type checking ensures all variants are handled

Notable Implementation Details

  • The rev-share-limit race algorithm avoids per-event truncation by computing base revenue from aggregated duration
  • Ranking for rev-share-limit uses pool claims as primary sort, duration as tie-breaker
  • All validation functions verify invariants for each metrics type
  • 30 new files added, 52 files total changed (+4,394, -1,602 lines)

Confidence Score: 4/5

  • This PR is safe to merge with careful monitoring of the rev-share-limit sequential race performance
  • Score of 4 reflects a well-architected, thoroughly tested refactoring with one consideration: the sequential race algorithm processes ALL events in chronological order, which could have performance implications at scale. The code quality is excellent with comprehensive validation, exhaustive type checking, and extensive test coverage. Forward compatibility is well-handled. The only reason this isn't a 5 is the potential performance concern of the sequential processing requirement for rev-share-limit.
  • Pay special attention to packages/ens-referrals/src/v1/award-models/rev-share-limit/leaderboard.ts (race algorithm performance) and apps/ensapi/src/lib/ensanalytics/referrer-leaderboard/database-v1.ts (query performance for large event sets)

Important Files Changed

Filename Overview
packages/ens-referrals/src/v1/award-models/rev-share-limit/leaderboard.ts Implements sequential race algorithm for rev-share-limit award model with deterministic event ordering and pool claim logic
packages/ens-referrals/src/v1/award-models/rev-share-limit/rules.ts Defines rev-share-limit rules with base revenue calculation constants and qualification logic
packages/ens-referrals/src/v1/award-models/shared/rules.ts Introduces base rules interface and ReferralProgramRulesUnrecognized type for forward compatibility with future award models
packages/ens-referrals/src/v1/api/zod-schemas.ts Refactored to support discriminated unions and preserve unrecognized award models during parsing for forward compatibility
apps/ensapi/src/lib/ensanalytics/referrer-leaderboard/database-v1.ts Adds getReferralEvents query with deterministic ordering for rev-share-limit sequential race processing
apps/ensapi/src/lib/ensanalytics/referrer-leaderboard/get-referrer-leaderboard-v1.ts Updated to dispatch to appropriate leaderboard builder based on award model with exhaustive type checking
packages/ens-referrals/src/v1/rules.ts Refactored from concrete interface to discriminated union of pie-split, rev-share-limit, and unrecognized rule types
apps/ensapi/src/cache/referral-program-edition-set.cache.ts Filters out unrecognized award model editions during loading to prevent server crashes from client-side forward compat types

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Client Request] --> B{Load Edition Config}
    B --> C[Parse with Zod Schema]
    C --> D{Award Model Type?}
    
    D -->|pie-split| E[Pie-Split Rules]
    D -->|rev-share-limit| F[Rev-Share-Limit Rules]
    D -->|unknown| G[Unrecognized Rules]
    
    G --> H[Filter at Cache Init]
    H --> I[Skip with Warning]
    
    E --> J{Build Leaderboard}
    F --> J
    
    J -->|pie-split| K[Get Aggregated Metrics<br/>GROUP BY query]
    J -->|rev-share-limit| L[Get Raw Events<br/>Individual rows]
    
    K --> M[Sort & Score<br/>Proportional Distribution]
    L --> N[Sequential Race<br/>First-Come First-Served]
    
    M --> O[Assign Ranks<br/>by Final Score]
    N --> P[Assign Ranks<br/>by Pool Claims]
    
    O --> Q[Serialize Response]
    P --> Q
    
    Q --> R{Discriminated Union}
    R -->|pie-split| S[Pie-Split Serialization]
    R -->|rev-share-limit| T[Rev-Share-Limit Serialization]
    
    S --> U[Return to Client]
    T --> U
Loading

Last reviewed commit: 1c0ab92

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

52 files reviewed, no comments

Edit Code Review Agent Settings | Greptile

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: 4

♻️ Duplicate comments (1)
packages/ens-referrals/src/v1/award-models/pie-split/metrics.ts (1)

317-318: ⚠️ Potential issue | 🟡 Minor

Remove redundant @returns JSDoc in buildUnrankedReferrerMetricsPieSplit.

This line repeats the function summary without adding new behavior detail.

📝 Proposed doc fix
 /**
  * Build an unranked zero-score referrer record for a referrer address that is not in the leaderboard.
  *
  * This is useful when you want to return a referrer record for an address that has no referrals
  * and is not qualified for the leaderboard.
  *
  * `@param` referrer - The referrer address
- * `@returns` An {`@link` UnrankedReferrerMetricsPieSplit} with zero values for all metrics and null rank
  */
As per coding guidelines, "Do not add JSDoc `@returns` tags that merely restate the method summary; remove redundancy during PR review".
🤖 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/pie-split/metrics.ts` around lines
317 - 318, The JSDoc for buildUnrankedReferrerMetricsPieSplit contains a
redundant `@returns` tag that restates the summary; remove that `@returns` line from
the function's docblock so the comment isn't repetitive, keeping the summary and
any meaningful tags (param/throws) intact and ensuring the docblock still
describes that the function returns an UnrankedReferrerMetricsPieSplit with zero
metrics and null rank.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@apps/ensapi/src/lib/ensanalytics/referrer-leaderboard/get-referrer-leaderboard-v1.test.ts`:
- Around line 69-76: The test assumes qualifiedReferrers always has a tail
element and uses a non-null assertion for lastQualifiedReferrer; make that
assertion conditional by checking whether qualifiedReferrers has at least one
element (or lastQualifiedReferrer !== undefined) before asserting
lastQualifiedReferrer[1].finalScoreBoost === 0, and keep the existing checks for
topQualifiedReferrers and unqualifiedReferrers unchanged; this removes the
brittle non-null assertion and handles fixtures smaller than
maxQualifiedReferrers.

In
`@apps/ensapi/src/lib/ensanalytics/referrer-leaderboard/get-referrer-leaderboard-v1.ts`:
- Around line 35-37: Instrument the "raw-event" rev-share-limit path by
measuring and emitting telemetry around the call to getReferralEvents and the
subsequent processing in buildReferrerLeaderboardRevShareLimit: record query
latency (start timer before getReferralEvents, stop after it resolves) and the
returned events row count, then emit both metrics (with contextual tags such as
rules identifier and accurateAsOf) to your existing telemetry/logging system;
ensure errors also emit a failure metric and include the same context so
volume/latency regressions on getReferralEvents and
buildReferrerLeaderboardRevShareLimit can be tracked.

In `@packages/ens-referrals/src/v1/api/zod-schemas.test.ts`:
- Around line 41-112: Add a test that supplies a recognized "rev-share-limit"
edition to the existing schema.parse path (same pattern as
pieSplitEdition/futureModelEdition) to ensure the recognized rev-share-limit
branch is exercised; create a revShareLimitEdition object similar to
pieSplitEdition (unique slug), parse with schema.parse([pieSplitEdition,
revShareLimitEdition]) and assert the parsed entry exists, that its
rules.awardModel equals ReferralProgramAwardModels.RevShareLimit, and that any
rev-share-specific fields (e.g., the limit/threshold field on the rules object)
are present and correctly parsed (types/values) so the
makeReferralProgramEditionConfigSetArraySchema path for rev-share-limit is
covered.

In
`@packages/ens-referrals/src/v1/award-models/rev-share-limit/leaderboard.test.ts`:
- Around line 51-60: The test helper makeEvent and its file-global
eventIdCounter produce implicit shared state across tests; add a test-suite
lifecycle reset (e.g., a beforeEach or afterEach in this test file) to set
eventIdCounter = 0 so each test starts with a clean ID sequence; reference
eventIdCounter and makeEvent so you can locate the helper and add the reset in
the same test file scope to avoid hidden coupling and tie-break flakiness.

---

Duplicate comments:
In `@packages/ens-referrals/src/v1/award-models/pie-split/metrics.ts`:
- Around line 317-318: The JSDoc for buildUnrankedReferrerMetricsPieSplit
contains a redundant `@returns` tag that restates the summary; remove that
`@returns` line from the function's docblock so the comment isn't repetitive,
keeping the summary and any meaningful tags (param/throws) intact and ensuring
the docblock still describes that the function returns an
UnrankedReferrerMetricsPieSplit with zero metrics and null rank.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b3e2803 and 1c0ab92.

📒 Files selected for processing (18)
  • .changeset/brave-eagles-award.md
  • apps/ensapi/src/cache/referral-program-edition-set.cache.ts
  • apps/ensapi/src/lib/ensanalytics/referrer-leaderboard/get-referrer-leaderboard-v1.test.ts
  • apps/ensapi/src/lib/ensanalytics/referrer-leaderboard/get-referrer-leaderboard-v1.ts
  • packages/ens-referrals/src/v1/api/serialize.ts
  • packages/ens-referrals/src/v1/api/serialized-types.ts
  • packages/ens-referrals/src/v1/api/zod-schemas.test.ts
  • packages/ens-referrals/src/v1/api/zod-schemas.ts
  • packages/ens-referrals/src/v1/award-models/pie-split/api/zod-schemas.ts
  • packages/ens-referrals/src/v1/award-models/pie-split/metrics.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/shared/api/zod-schemas.ts
  • packages/ens-referrals/src/v1/award-models/shared/rules.ts
  • packages/ens-referrals/src/v1/client.ts
  • packages/ens-referrals/src/v1/leaderboard-page.test.ts
  • packages/ens-referrals/src/v1/rules.ts

Comment on lines +35 to +37
const events = await getReferralEvents(rules);
return buildReferrerLeaderboardRevShareLimit(events, rules, accurateAsOf);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Add telemetry guardrails for the raw-event branch.

The rev-share-limit path pulls full event streams; consider tracking row count and query latency to catch volume regressions early.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/ensapi/src/lib/ensanalytics/referrer-leaderboard/get-referrer-leaderboard-v1.ts`
around lines 35 - 37, Instrument the "raw-event" rev-share-limit path by
measuring and emitting telemetry around the call to getReferralEvents and the
subsequent processing in buildReferrerLeaderboardRevShareLimit: record query
latency (start timer before getReferralEvents, stop after it resolves) and the
returned events row count, then emit both metrics (with contextual tags such as
rules identifier and accurateAsOf) to your existing telemetry/logging system;
ensure errors also emit a failure metric and include the same context so
volume/latency regressions on getReferralEvents and
buildReferrerLeaderboardRevShareLimit can be tracked.

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

♻️ Duplicate comments (1)
packages/ens-referrals/src/v1/award-models/pie-split/metrics.ts (1)

310-318: 🧹 Nitpick | 🔵 Trivial

Redundant @returns tag restates the method summary.

Line 317's @returns ("An {@link UnrankedReferrerMetricsPieSplit} with zero values for all metrics and null rank") largely duplicates the summary on line 311 ("Build an unranked zero-score referrer record…").

📝 Proposed fix
 /**
  * Build an unranked zero-score referrer record for a referrer address that is not in the leaderboard.
  *
  * This is useful when you want to return a referrer record for an address that has no referrals
  * and is not qualified for the leaderboard.
  *
  * `@param` referrer - The referrer address
- * `@returns` An {`@link` UnrankedReferrerMetricsPieSplit} with zero values for all metrics and null rank
  */

As per coding guidelines, "Do not add JSDoc @returns tags that merely restate the method summary; remove redundancy during PR review."

🤖 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/pie-split/metrics.ts` around lines
310 - 318, Remove the redundant JSDoc `@returns` line that restates the summary
for the function that builds an unranked zero-score referrer record (the JSDoc
block that documents returning an UnrankedReferrerMetricsPieSplit); leave the
descriptive summary and any non-redundant tags intact so the doc comment only
provides unique information.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@apps/ensapi/src/lib/ensanalytics/referrer-leaderboard/get-referrer-leaderboard-v1.ts`:
- Around line 38-43: In the Unrecognized switch branch of getReferrerLeaderboard
(the case for ReferralProgramAwardModels.Unrecognized) TypeScript can't
guarantee that rules has originalAwardModel, so explicitly narrow the type
before accessing it: cast rules to ReferralProgramRulesUnrecognized when
interpolating originalAwardModel in the thrown Error message (reference symbols:
ReferralProgramAwardModels.Unrecognized, ReferralProgramRulesUnrecognized, and
the rules variable).

In `@packages/ens-referrals/src/v1/award-models/pie-split/metrics.ts`:
- Around line 147-153: Remove the redundant JSDoc `@returns` line in the JSDoc
block that begins with "Calculate the share of the award pool for a referrer"
(the comment above the function that computes a referrer's award pool share),
leaving the summary and parameter tags intact; do not add a restating `@returns`
tag—rely on the function's TypeScript signature for return typing instead.
- Around line 154-157: The JSDoc for calcReferrerAwardPoolSharePieSplit mentions
a `rules` parameter that no longer exists in the function signature; update the
documentation to match the code by removing the `@param rules` entry (or, if
`rules` is actually required, add it to the function signature and use it).
Specifically, edit the JSDoc above calcReferrerAwardPoolSharePieSplit so it
reflects only the actual parameters `referrer: RankedReferrerMetricsPieSplit`
and `aggregatedMetrics: AggregatedReferrerMetricsPieSplit`, removing the stale
`rules` reference to avoid confusion.

---

Duplicate comments:
In `@packages/ens-referrals/src/v1/award-models/pie-split/metrics.ts`:
- Around line 310-318: Remove the redundant JSDoc `@returns` line that restates
the summary for the function that builds an unranked zero-score referrer record
(the JSDoc block that documents returning an UnrankedReferrerMetricsPieSplit);
leave the descriptive summary and any non-redundant tags intact so the doc
comment only provides unique information.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b3e2803 and 9639022.

📒 Files selected for processing (19)
  • .changeset/brave-eagles-award.md
  • apps/ensapi/src/cache/referral-program-edition-set.cache.ts
  • apps/ensapi/src/handlers/ensanalytics-api-v1.test.ts
  • apps/ensapi/src/lib/ensanalytics/referrer-leaderboard/get-referrer-leaderboard-v1.test.ts
  • apps/ensapi/src/lib/ensanalytics/referrer-leaderboard/get-referrer-leaderboard-v1.ts
  • packages/ens-referrals/src/v1/api/serialize.ts
  • packages/ens-referrals/src/v1/api/serialized-types.ts
  • packages/ens-referrals/src/v1/api/zod-schemas.test.ts
  • packages/ens-referrals/src/v1/api/zod-schemas.ts
  • packages/ens-referrals/src/v1/award-models/pie-split/api/zod-schemas.ts
  • packages/ens-referrals/src/v1/award-models/pie-split/metrics.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/shared/api/zod-schemas.ts
  • packages/ens-referrals/src/v1/award-models/shared/rules.ts
  • packages/ens-referrals/src/v1/client.ts
  • packages/ens-referrals/src/v1/leaderboard-page.test.ts
  • packages/ens-referrals/src/v1/rules.ts

Copilot AI review requested due to automatic review settings February 27, 2026 16:44
@vercel vercel bot temporarily deployed to Preview – ensnode.io February 27, 2026 16:44 Inactive
@vercel vercel bot temporarily deployed to Preview – admin.ensnode.io February 27, 2026 16:44 Inactive
@vercel vercel bot temporarily deployed to Preview – ensrainbow.io February 27, 2026 16:44 Inactive
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 52 out of 52 changed files in this pull request and generated no new comments.


💡 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: 1

♻️ Duplicate comments (2)
packages/ens-referrals/src/v1/award-models/pie-split/metrics.ts (2)

74-80: 🛠️ Refactor suggestion | 🟠 Major

Lift reusable 0..1 invariants to a shared type alias.

The context-independent range invariant is repeated inline on finalScoreBoost and awardPoolShare. Keep field-level formula invariants there, but move the reusable 0..1 invariant onto one alias used by both fields.

♻️ Proposed refactor
+/** `@invariant` Guaranteed to be a number between 0 and 1 (inclusive). */
+export type UnitInterval = number;
+
 export interface RankedReferrerMetricsPieSplit extends ScoredReferrerMetricsPieSplit {
@@
   /**
    * The referrer's final score boost.
    *
-   * `@invariant` Guaranteed to be a number between 0 and 1 (inclusive)
    * `@invariant` Calculated as: `1-((rank-1)/({`@link` ReferralProgramRulesPieSplit.maxQualifiedReferrers}-1))` if `isQualified` is `true`, else `0`
    */
-  finalScoreBoost: number;
+  finalScoreBoost: UnitInterval;
@@
 export interface AwardedReferrerMetricsPieSplit extends RankedReferrerMetricsPieSplit {
@@
   /**
    * The referrer's share of the award pool.
    *
-   * `@invariant` Guaranteed to be a number between 0 and 1 (inclusive)
    * `@invariant` Calculated as: `finalScore / {`@link` AggregatedReferrerMetricsPieSplit.grandTotalQualifiedReferrersFinalScore}` if `isQualified` is `true`, else `0`
    */
-  awardPoolShare: number;
+  awardPoolShare: UnitInterval;

Based on learnings, keep context-dependent invariants as field JSDoc and move only context-independent/reused invariants to type aliases.

Also applies to: 171-174

🤖 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/pie-split/metrics.ts` around lines
74 - 80, Create a shared type alias for the reusable "0..1" invariant (e.g.,
ZeroToOne) and use it for both finalScoreBoost and awardPoolShare instead of
repeating the range JSDoc; keep the field-specific formula invariants (the
rank-based calculation) on the individual properties (finalScoreBoost,
awardPoolShare) but move the context-independent "Guaranteed to be a number
between 0 and 1 (inclusive)" text into the alias JSDoc and replace the
properties' type from number to the new alias; apply the same change to the
other occurrence noted (the block around the comments at the second location
referenced, lines 171-174).

147-152: ⚠️ Potential issue | 🟡 Minor

Remove redundant @returns tags in JSDoc blocks.

Both JSDoc @returns entries restate the summary and should be removed for consistency.

✂️ Proposed cleanup
 /**
  * Calculate the share of the award pool for a referrer.
  * `@param` referrer - The referrer to calculate the award pool share for.
  * `@param` aggregatedMetrics - Aggregated metrics for all referrers.
- * `@returns` The referrer's share of the award pool as a number between 0 and 1 (inclusive).
  */
 export const calcReferrerAwardPoolSharePieSplit = (
 /**
  * Build an unranked zero-score referrer record for a referrer address that is not in the leaderboard.
  *
  * This is useful when you want to return a referrer record for an address that has no referrals
  * and is not qualified for the leaderboard.
  *
  * `@param` referrer - The referrer address
- * `@returns` An {`@link` UnrankedReferrerMetricsPieSplit} with zero values for all metrics and null rank
  */
 export const buildUnrankedReferrerMetricsPieSplit = (

As per coding guidelines, "Do not add JSDoc @returns tags that merely restate the method summary; remove redundancy during PR review."

Also applies to: 309-317

🤖 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/pie-split/metrics.ts` around lines
147 - 152, Remove the redundant `@returns` JSDoc tags that merely repeat the
summary in the JSDoc block above the function that starts with "Calculate the
share of the award pool for a referrer" (the block that documents params
referrer and aggregatedMetrics), and do the same for the similar JSDoc block
around lines 309-317; keep the summary sentence and parameter tags, but delete
the duplicate `@returns` lines so the doc is not repetitive.
🤖 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/pie-split/metrics.ts`:
- Around line 185-205: The validator validateAwardedReferrerMetricsPieSplit
currently only checks shape and bounds; update it to also assert that
referrer.awardPoolApproxValue equals the derived value from the declared formula
by computing scalePrice(rules.totalAwardPoolValue, referrer.awardPoolShare) and
comparing the resulting price/amount to referrer.awardPoolApproxValue (after
parsing with
makePriceUsdcSchema("AwardedReferrerMetricsPieSplit.awardPoolApproxValue")). If
they differ, throw a clear Error indicating the mismatch and include both the
expected and actual amounts; keep the existing bounds and schema validation
(validateRankedReferrerMetricsPieSplit and makePriceUsdcSchema) in place and
perform the formula-check after parsing and before the upper-bound check.

---

Duplicate comments:
In `@packages/ens-referrals/src/v1/award-models/pie-split/metrics.ts`:
- Around line 74-80: Create a shared type alias for the reusable "0..1"
invariant (e.g., ZeroToOne) and use it for both finalScoreBoost and
awardPoolShare instead of repeating the range JSDoc; keep the field-specific
formula invariants (the rank-based calculation) on the individual properties
(finalScoreBoost, awardPoolShare) but move the context-independent "Guaranteed
to be a number between 0 and 1 (inclusive)" text into the alias JSDoc and
replace the properties' type from number to the new alias; apply the same change
to the other occurrence noted (the block around the comments at the second
location referenced, lines 171-174).
- Around line 147-152: Remove the redundant `@returns` JSDoc tags that merely
repeat the summary in the JSDoc block above the function that starts with
"Calculate the share of the award pool for a referrer" (the block that documents
params referrer and aggregatedMetrics), and do the same for the similar JSDoc
block around lines 309-317; keep the summary sentence and parameter tags, but
delete the duplicate `@returns` lines so the doc is not repetitive.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9639022 and 8d6ae5c.

📒 Files selected for processing (1)
  • packages/ens-referrals/src/v1/award-models/pie-split/metrics.ts

Comment on lines +185 to +205
export const validateAwardedReferrerMetricsPieSplit = (
referrer: AwardedReferrerMetricsPieSplit,
rules: ReferralProgramRulesPieSplit,
): void => {
validateRankedReferrerMetricsPieSplit(referrer, rules);
if (referrer.awardPoolShare < 0 || referrer.awardPoolShare > 1) {
throw new Error(
`Invalid AwardedReferrerMetricsPieSplit: ${referrer.awardPoolShare}. awardPoolShare must be between 0 and 1 (inclusive).`,
);
}

makePriceUsdcSchema("AwardedReferrerMetricsPieSplit.awardPoolApproxValue").parse(
referrer.awardPoolApproxValue,
);

if (referrer.awardPoolApproxValue.amount > rules.totalAwardPoolValue.amount) {
throw new Error(
`AwardedReferrerMetricsPieSplit: awardPoolApproxValue.amount ${referrer.awardPoolApproxValue.amount.toString()} exceeds totalAwardPoolValue.amount ${rules.totalAwardPoolValue.amount.toString()}.`,
);
}
};
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Validate awardPoolApproxValue against the declared formula, not just bounds.

Line 196 validates shape and Line 200 validates only an upper bound. A payload with a stale/mismatched awardPoolApproxValue.amount can still pass, even when it does not equal scalePrice(rules.totalAwardPoolValue, awardPoolShare).

🛠️ Proposed fix
 export const validateAwardedReferrerMetricsPieSplit = (
   referrer: AwardedReferrerMetricsPieSplit,
   rules: ReferralProgramRulesPieSplit,
 ): void => {
   validateRankedReferrerMetricsPieSplit(referrer, rules);
   if (referrer.awardPoolShare < 0 || referrer.awardPoolShare > 1) {
     throw new Error(
       `Invalid AwardedReferrerMetricsPieSplit: ${referrer.awardPoolShare}. awardPoolShare must be between 0 and 1 (inclusive).`,
     );
   }

   makePriceUsdcSchema("AwardedReferrerMetricsPieSplit.awardPoolApproxValue").parse(
     referrer.awardPoolApproxValue,
   );
+  const expectedAwardPoolApproxValue = scalePrice(
+    rules.totalAwardPoolValue,
+    referrer.awardPoolShare,
+  );
+  if (referrer.awardPoolApproxValue.amount !== expectedAwardPoolApproxValue.amount) {
+    throw new Error(
+      `AwardedReferrerMetricsPieSplit: Invalid awardPoolApproxValue.amount ${referrer.awardPoolApproxValue.amount.toString()}, expected ${expectedAwardPoolApproxValue.amount.toString()} from awardPoolShare ${referrer.awardPoolShare}.`,
+    );
+  }

   if (referrer.awardPoolApproxValue.amount > rules.totalAwardPoolValue.amount) {
     throw new Error(
       `AwardedReferrerMetricsPieSplit: awardPoolApproxValue.amount ${referrer.awardPoolApproxValue.amount.toString()} exceeds totalAwardPoolValue.amount ${rules.totalAwardPoolValue.amount.toString()}.`,
     );
   }
 };
🤖 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/pie-split/metrics.ts` around lines
185 - 205, The validator validateAwardedReferrerMetricsPieSplit currently only
checks shape and bounds; update it to also assert that
referrer.awardPoolApproxValue equals the derived value from the declared formula
by computing scalePrice(rules.totalAwardPoolValue, referrer.awardPoolShare) and
comparing the resulting price/amount to referrer.awardPoolApproxValue (after
parsing with
makePriceUsdcSchema("AwardedReferrerMetricsPieSplit.awardPoolApproxValue")). If
they differ, throw a clear Error indicating the mismatch and include both the
expected and actual amounts; keep the existing bounds and schema validation
(validateRankedReferrerMetricsPieSplit and makePriceUsdcSchema) in place and
perform the formula-check after parsing and before the upper-bound check.

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.

Add data model customizability for specific referral program editions

2 participants