From f5b4ecc6c9066c7fae1593afadabc1ee2a02bfc9 Mon Sep 17 00:00:00 2001 From: Goader Date: Mon, 6 Apr 2026 12:17:33 +0200 Subject: [PATCH 01/11] renamed variables --- .changeset/bright-foxes-dance.md | 5 + packages/ens-referrals/README.md | 8 +- .../src/v1/api/zod-schemas.test.ts | 30 ++--- .../rev-share-limit/api/serialize.ts | 12 +- .../rev-share-limit/api/serialized-types.ts | 20 ++-- .../rev-share-limit/api/zod-schemas.ts | 33 +++--- .../rev-share-limit/leaderboard.test.ts | 106 +++++++++--------- .../rev-share-limit/leaderboard.ts | 12 +- .../award-models/rev-share-limit/metrics.ts | 58 +++++----- .../v1/award-models/rev-share-limit/rules.ts | 26 ++--- 10 files changed, 156 insertions(+), 154 deletions(-) create mode 100644 .changeset/bright-foxes-dance.md diff --git a/.changeset/bright-foxes-dance.md b/.changeset/bright-foxes-dance.md new file mode 100644 index 0000000000..36ba8b0aa6 --- /dev/null +++ b/.changeset/bright-foxes-dance.md @@ -0,0 +1,5 @@ +--- +"@namehash/ens-referrals": minor +--- + +Rename rev-share-limit API fields for clarity: `minQualifiedRevenueContribution` → `minBaseRevenueContribution`, `qualifiedRevenueShare` → `maxBaseRevenueShare`, `standardAwardValue` → `uncappedAwardValue`, `awardPoolApproxValue` → `cappedAwardValue`. diff --git a/packages/ens-referrals/README.md b/packages/ens-referrals/README.md index b1d724a58e..6cb6494dba 100644 --- a/packages/ens-referrals/README.md +++ b/packages/ens-referrals/README.md @@ -98,11 +98,11 @@ if (response.responseCode === ReferrerLeaderboardPageResponseCodes.Ok) { if (leaderboardPage.awardModel === ReferralProgramAwardModels.RevShareLimit) { console.log( - `Min Qualified Revenue Contribution: ${leaderboardPage.rules.minQualifiedRevenueContribution}`, + `Min Base Revenue Contribution: ${leaderboardPage.rules.minBaseRevenueContribution}`, ); - console.log(`Qualified Revenue Share: ${leaderboardPage.rules.qualifiedRevenueShare}`); + console.log(`Max Base Revenue Share: ${leaderboardPage.rules.maxBaseRevenueShare}`); console.log( - `Tentative award for the best referrer: ${firstReferrer !== null ? firstReferrer.awardPoolApproxValue : noReferrersFallback}`, + `Tentative award for the best referrer: ${firstReferrer !== null ? firstReferrer.cappedAwardValue : noReferrersFallback}`, ); } } @@ -146,7 +146,7 @@ if (response.responseCode === ReferrerMetricsEditionsResponseCodes.Ok) { console.log( `Referrer's total base revenue contribution: ${detail.referrer.totalBaseRevenueContribution}`, ); - console.log(`Referrer's standard award value: ${detail.referrer.standardAwardValue}`); + console.log(`Referrer's uncapped award value: ${detail.referrer.uncappedAwardValue}`); } } } diff --git a/packages/ens-referrals/src/v1/api/zod-schemas.test.ts b/packages/ens-referrals/src/v1/api/zod-schemas.test.ts index 82bcff6104..cb79b93efc 100644 --- a/packages/ens-referrals/src/v1/api/zod-schemas.test.ts +++ b/packages/ens-referrals/src/v1/api/zod-schemas.test.ts @@ -44,8 +44,8 @@ describe("makeReferralProgramEditionConfigSetArraySchema", () => { rules: { awardModel: ReferralProgramAwardModels.RevShareLimit, totalAwardPoolValue: parseUsdc("500"), - minQualifiedRevenueContribution: parseUsdc("10"), - qualifiedRevenueShare: 0.5, + minBaseRevenueContribution: parseUsdc("10"), + maxBaseRevenueShare: 0.5, startTime: 1000000, endTime: 2000000, subregistryId, @@ -93,13 +93,13 @@ describe("makeReferralProgramEditionConfigSetArraySchema", () => { const rules = revShareLimit!.rules as { awardModel: typeof ReferralProgramAwardModels.RevShareLimit; totalAwardPoolValue: { amount: bigint; currency: string }; - minQualifiedRevenueContribution: { amount: bigint; currency: string }; - qualifiedRevenueShare: number; + minBaseRevenueContribution: { amount: bigint; currency: string }; + maxBaseRevenueShare: number; }; expect(rules.totalAwardPoolValue).toBeDefined(); - expect(rules.minQualifiedRevenueContribution).toBeDefined(); - expect(typeof rules.qualifiedRevenueShare).toBe("number"); - expect(rules.qualifiedRevenueShare).toBe(0.5); + expect(rules.minBaseRevenueContribution).toBeDefined(); + expect(typeof rules.maxBaseRevenueShare).toBe("number"); + expect(rules.maxBaseRevenueShare).toBe(0.5); expect(revShareLimit!.rules.areAwardsDistributed).toBe( revShareLimitEdition.rules.areAwardsDistributed, ); @@ -215,8 +215,8 @@ describe("makeReferrerLeaderboardPageSchema", () => { rules: { awardModel: ReferralProgramAwardModels.RevShareLimit, totalAwardPoolValue: parseUsdc("2000"), - minQualifiedRevenueContribution: parseUsdc("10"), - qualifiedRevenueShare: 0.5, + minBaseRevenueContribution: parseUsdc("10"), + maxBaseRevenueShare: 0.5, startTime: 1000000, endTime: 2000000, subregistryId, @@ -325,8 +325,8 @@ describe("makeReferralProgramEditionSummarySchema", () => { rules: { awardModel: ReferralProgramAwardModels.RevShareLimit, totalAwardPoolValue: parseUsdc("2000"), - minQualifiedRevenueContribution: parseUsdc("10"), - qualifiedRevenueShare: 0.5, + minBaseRevenueContribution: parseUsdc("10"), + maxBaseRevenueShare: 0.5, startTime: 1000000, endTime: 2000000, subregistryId, @@ -507,8 +507,8 @@ describe("makeReferrerEditionMetricsSchema", () => { rules: { awardModel: ReferralProgramAwardModels.RevShareLimit, totalAwardPoolValue: parseUsdc("2000"), - minQualifiedRevenueContribution: parseUsdc("10"), - qualifiedRevenueShare: 0.5, + minBaseRevenueContribution: parseUsdc("10"), + maxBaseRevenueShare: 0.5, startTime: 1000000, endTime: 2000000, subregistryId, @@ -523,8 +523,8 @@ describe("makeReferrerEditionMetricsSchema", () => { totalBaseRevenueContribution: parseUsdc("150"), rank: 1, isQualified: true, - standardAwardValue: parseUsdc("200"), - awardPoolApproxValue: parseUsdc("200"), + uncappedAwardValue: parseUsdc("200"), + cappedAwardValue: parseUsdc("200"), isAdminDisqualified: false, adminDisqualificationReason: null, }, diff --git a/packages/ens-referrals/src/v1/award-models/rev-share-limit/api/serialize.ts b/packages/ens-referrals/src/v1/award-models/rev-share-limit/api/serialize.ts index 3dfa697e30..a1966c87bc 100644 --- a/packages/ens-referrals/src/v1/award-models/rev-share-limit/api/serialize.ts +++ b/packages/ens-referrals/src/v1/award-models/rev-share-limit/api/serialize.ts @@ -35,8 +35,8 @@ export function serializeReferralProgramRulesRevShareLimit( return { awardModel: rules.awardModel, totalAwardPoolValue: serializePriceUsdc(rules.totalAwardPoolValue), - minQualifiedRevenueContribution: serializePriceUsdc(rules.minQualifiedRevenueContribution), - qualifiedRevenueShare: rules.qualifiedRevenueShare, + minBaseRevenueContribution: serializePriceUsdc(rules.minBaseRevenueContribution), + maxBaseRevenueShare: rules.maxBaseRevenueShare, startTime: rules.startTime, endTime: rules.endTime, subregistryId: rules.subregistryId, @@ -74,8 +74,8 @@ export function serializeAwardedReferrerMetricsRevShareLimit( totalBaseRevenueContribution: serializePriceUsdc(metrics.totalBaseRevenueContribution), rank: metrics.rank, isQualified: metrics.isQualified, - standardAwardValue: serializePriceUsdc(metrics.standardAwardValue), - awardPoolApproxValue: serializePriceUsdc(metrics.awardPoolApproxValue), + uncappedAwardValue: serializePriceUsdc(metrics.uncappedAwardValue), + cappedAwardValue: serializePriceUsdc(metrics.cappedAwardValue), isAdminDisqualified: metrics.isAdminDisqualified, adminDisqualificationReason: metrics.adminDisqualificationReason, }; @@ -95,8 +95,8 @@ export function serializeUnrankedReferrerMetricsRevShareLimit( totalBaseRevenueContribution: serializePriceUsdc(metrics.totalBaseRevenueContribution), rank: metrics.rank, isQualified: metrics.isQualified, - standardAwardValue: serializePriceUsdc(metrics.standardAwardValue), - awardPoolApproxValue: serializePriceUsdc(metrics.awardPoolApproxValue), + uncappedAwardValue: serializePriceUsdc(metrics.uncappedAwardValue), + cappedAwardValue: serializePriceUsdc(metrics.cappedAwardValue), isAdminDisqualified: metrics.isAdminDisqualified, adminDisqualificationReason: metrics.adminDisqualificationReason, }; diff --git a/packages/ens-referrals/src/v1/award-models/rev-share-limit/api/serialized-types.ts b/packages/ens-referrals/src/v1/award-models/rev-share-limit/api/serialized-types.ts index 96807091cf..db0b42af87 100644 --- a/packages/ens-referrals/src/v1/award-models/rev-share-limit/api/serialized-types.ts +++ b/packages/ens-referrals/src/v1/award-models/rev-share-limit/api/serialized-types.ts @@ -20,10 +20,10 @@ import type { ReferralProgramRulesRevShareLimit } from "../rules"; export interface SerializedReferralProgramRulesRevShareLimit extends Omit< ReferralProgramRulesRevShareLimit, - "totalAwardPoolValue" | "minQualifiedRevenueContribution" | "rulesUrl" + "totalAwardPoolValue" | "minBaseRevenueContribution" | "rulesUrl" > { totalAwardPoolValue: SerializedPriceUsdc; - minQualifiedRevenueContribution: SerializedPriceUsdc; + minBaseRevenueContribution: SerializedPriceUsdc; rulesUrl: string; } @@ -47,13 +47,13 @@ export interface SerializedAwardedReferrerMetricsRevShareLimit AwardedReferrerMetricsRevShareLimit, | "totalRevenueContribution" | "totalBaseRevenueContribution" - | "standardAwardValue" - | "awardPoolApproxValue" + | "uncappedAwardValue" + | "cappedAwardValue" > { totalRevenueContribution: SerializedPriceEth; totalBaseRevenueContribution: SerializedPriceUsdc; - standardAwardValue: SerializedPriceUsdc; - awardPoolApproxValue: SerializedPriceUsdc; + uncappedAwardValue: SerializedPriceUsdc; + cappedAwardValue: SerializedPriceUsdc; } /** @@ -64,13 +64,13 @@ export interface SerializedUnrankedReferrerMetricsRevShareLimit UnrankedReferrerMetricsRevShareLimit, | "totalRevenueContribution" | "totalBaseRevenueContribution" - | "standardAwardValue" - | "awardPoolApproxValue" + | "uncappedAwardValue" + | "cappedAwardValue" > { totalRevenueContribution: SerializedPriceEth; totalBaseRevenueContribution: SerializedPriceUsdc; - standardAwardValue: SerializedPriceUsdc; - awardPoolApproxValue: SerializedPriceUsdc; + uncappedAwardValue: SerializedPriceUsdc; + cappedAwardValue: SerializedPriceUsdc; } /** diff --git a/packages/ens-referrals/src/v1/award-models/rev-share-limit/api/zod-schemas.ts b/packages/ens-referrals/src/v1/award-models/rev-share-limit/api/zod-schemas.ts index 9578357b95..cab71a8158 100644 --- a/packages/ens-referrals/src/v1/award-models/rev-share-limit/api/zod-schemas.ts +++ b/packages/ens-referrals/src/v1/award-models/rev-share-limit/api/zod-schemas.ts @@ -41,12 +41,11 @@ export const makeReferralProgramRulesRevShareLimitSchema = ( makeBaseReferralProgramRulesSchema(valueLabel).safeExtend({ awardModel: z.literal(ReferralProgramAwardModels.RevShareLimit), totalAwardPoolValue: makePriceUsdcSchema(`${valueLabel}.totalAwardPoolValue`), - minQualifiedRevenueContribution: makePriceUsdcSchema( - `${valueLabel}.minQualifiedRevenueContribution`, + minBaseRevenueContribution: makePriceUsdcSchema(`${valueLabel}.minBaseRevenueContribution`), + maxBaseRevenueShare: makeFiniteNonNegativeNumberSchema(`${valueLabel}.maxBaseRevenueShare`).max( + 1, + `${valueLabel}.maxBaseRevenueShare must be <= 1`, ), - qualifiedRevenueShare: makeFiniteNonNegativeNumberSchema( - `${valueLabel}.qualifiedRevenueShare`, - ).max(1, `${valueLabel}.qualifiedRevenueShare must be <= 1`), disqualifications: z .array( makeReferralProgramEditionDisqualificationSchema(`${valueLabel}.disqualifications[item]`), @@ -80,8 +79,8 @@ export const makeAwardedReferrerMetricsRevShareLimitSchema = ( ), rank: makePositiveIntegerSchema(`${valueLabel}.rank`), isQualified: z.boolean(), - standardAwardValue: makePriceUsdcSchema(`${valueLabel}.standardAwardValue`), - awardPoolApproxValue: makePriceUsdcSchema(`${valueLabel}.awardPoolApproxValue`), + uncappedAwardValue: makePriceUsdcSchema(`${valueLabel}.uncappedAwardValue`), + cappedAwardValue: makePriceUsdcSchema(`${valueLabel}.cappedAwardValue`), isAdminDisqualified: z.boolean(), adminDisqualificationReason: z .string() @@ -89,16 +88,16 @@ export const makeAwardedReferrerMetricsRevShareLimitSchema = ( .min(1, `${valueLabel}.adminDisqualificationReason must not be empty`) .nullable(), }) - .refine((data) => data.awardPoolApproxValue.amount <= data.standardAwardValue.amount, { - message: `${valueLabel}.awardPoolApproxValue must be <= ${valueLabel}.standardAwardValue`, - path: ["awardPoolApproxValue"], + .refine((data) => data.cappedAwardValue.amount <= data.uncappedAwardValue.amount, { + message: `${valueLabel}.cappedAwardValue must be <= ${valueLabel}.uncappedAwardValue`, + path: ["cappedAwardValue"], }) .refine( (data) => !data.isAdminDisqualified || - (data.isQualified === false && data.awardPoolApproxValue.amount === 0n), + (data.isQualified === false && data.cappedAwardValue.amount === 0n), { - message: `When ${valueLabel}.isAdminDisqualified is true, isQualified must be false and awardPoolApproxValue.amount must be 0`, + message: `When ${valueLabel}.isAdminDisqualified is true, isQualified must be false and cappedAwardValue.amount must be 0`, path: ["isAdminDisqualified"], }, ) @@ -124,8 +123,8 @@ export const makeUnrankedReferrerMetricsRevShareLimitSchema = ( ), rank: z.null(), isQualified: z.literal(false), - standardAwardValue: makePriceUsdcSchema(`${valueLabel}.standardAwardValue`), - awardPoolApproxValue: makePriceUsdcSchema(`${valueLabel}.awardPoolApproxValue`), + uncappedAwardValue: makePriceUsdcSchema(`${valueLabel}.uncappedAwardValue`), + cappedAwardValue: makePriceUsdcSchema(`${valueLabel}.cappedAwardValue`), isAdminDisqualified: z.boolean(), adminDisqualificationReason: z .string() @@ -133,9 +132,9 @@ export const makeUnrankedReferrerMetricsRevShareLimitSchema = ( .min(1, `${valueLabel}.adminDisqualificationReason must not be empty`) .nullable(), }) - .refine((data) => data.awardPoolApproxValue.amount <= data.standardAwardValue.amount, { - message: `${valueLabel}.awardPoolApproxValue must be <= ${valueLabel}.standardAwardValue`, - path: ["awardPoolApproxValue"], + .refine((data) => data.cappedAwardValue.amount <= data.uncappedAwardValue.amount, { + message: `${valueLabel}.cappedAwardValue must be <= ${valueLabel}.uncappedAwardValue`, + path: ["cappedAwardValue"], }) .refine((data) => data.isAdminDisqualified === (data.adminDisqualificationReason !== null), { message: `${valueLabel}.adminDisqualificationReason must be non-null iff isAdminDisqualified is true`, diff --git a/packages/ens-referrals/src/v1/award-models/rev-share-limit/leaderboard.test.ts b/packages/ens-referrals/src/v1/award-models/rev-share-limit/leaderboard.test.ts index 79d92fcd67..74b4d879bc 100644 --- a/packages/ens-referrals/src/v1/award-models/rev-share-limit/leaderboard.test.ts +++ b/packages/ens-referrals/src/v1/award-models/rev-share-limit/leaderboard.test.ts @@ -37,22 +37,22 @@ const CHECKPOINT_PREFIX = * Build test rules. * * - BASE_REVENUE_CONTRIBUTION_PER_YEAR = $5 USDC - * - qualifiedRevenueShare = 0.5 + * - maxBaseRevenueShare = 0.5 * - 1 year of duration → $5 base revenue → $2.50 standard award - * - minQualifiedRevenueContribution = $5 → need exactly 1 year to qualify + * - minBaseRevenueContribution = $5 → need exactly 1 year to qualify * * @param totalAwardPoolValue - USDC amount for the pool (default: $1000) - * @param minQualifiedRevenueContribution - USDC threshold (default: $5 = 1 year) + * @param minBaseRevenueContribution - USDC threshold (default: $5 = 1 year) */ function buildTestRules( totalAwardPoolValue = parseUsdc("1000"), - minQualifiedRevenueContribution = parseUsdc("5"), + minBaseRevenueContribution = parseUsdc("5"), disqualifications: ReferralProgramEditionDisqualification[] = [], ) { return buildReferralProgramRulesRevShareLimit( totalAwardPoolValue, - minQualifiedRevenueContribution, - 0.5, // qualifiedRevenueShare + minBaseRevenueContribution, + 0.5, // maxBaseRevenueShare parseTimestamp("2026-01-01T00:00:00Z"), parseTimestamp("2026-12-31T23:59:59Z"), { chainId: 1, address: "0x57f1887a8bf19b14fc0df6fd9b2acc9af147ea85" }, @@ -114,7 +114,7 @@ describe("buildReferrerLeaderboardRevShareLimit", () => { }); describe("Scenario A — unqualified referrer: no award claimed", () => { - it("accumulates standard award but awardPoolApproxValue is $0 when not qualified", () => { + it("accumulates standard award but cappedAwardValue is $0 when not qualified", () => { // Half a year of duration → base revenue = $2.50 (< $5 threshold) const events = [makeEvent(ADDR_A, 1000, Math.floor(SECONDS_PER_YEAR / 2))]; const rules = buildTestRules(); @@ -124,9 +124,9 @@ describe("buildReferrerLeaderboardRevShareLimit", () => { expect(referrer).toBeDefined(); expect(referrer.isQualified).toBe(false); - // standardAwardValue = 0.5 × ($5 × 0.5 years) = 0.5 × $2.50 = $1.25 - expect(referrer.standardAwardValue.amount).toBe(parseUsdc("1.25").amount); - expect(referrer.awardPoolApproxValue.amount).toBe(0n); + // uncappedAwardValue = 0.5 × ($5 × 0.5 years) = 0.5 × $2.50 = $1.25 + expect(referrer.uncappedAwardValue.amount).toBe(parseUsdc("1.25").amount); + expect(referrer.cappedAwardValue.amount).toBe(0n); // Pool should be fully intact expect(result.aggregatedMetrics.awardPoolRemaining.amount).toBe( @@ -150,14 +150,14 @@ describe("buildReferrerLeaderboardRevShareLimit", () => { const referrer = result.referrers.get(ADDR_A)!; expect(referrer.isQualified).toBe(true); - expect(referrer.standardAwardValue.amount).toBe(STANDARD_AWARD_1Y.amount); + expect(referrer.uncappedAwardValue.amount).toBe(STANDARD_AWARD_1Y.amount); // Claims all accumulated: 2 × $1.25 = $2.50 - expect(referrer.awardPoolApproxValue.amount).toBe(STANDARD_AWARD_1Y.amount); + expect(referrer.cappedAwardValue.amount).toBe(STANDARD_AWARD_1Y.amount); }); }); describe("Scenario B-2 — just qualifies, but pool is too small to cover full accumulated award", () => { - it("awardPoolApproxValue is capped by remaining pool when qualifying", () => { + it("cappedAwardValue is capped by remaining pool when qualifying", () => { // Same as Scenario B but pool only has $1.50 const poolAmount = parseUsdc("1.5"); const rules = buildTestRules(poolAmount); @@ -170,10 +170,10 @@ describe("buildReferrerLeaderboardRevShareLimit", () => { const referrer = result.referrers.get(ADDR_A)!; expect(referrer.isQualified).toBe(true); - // standardAwardValue = $2.50 (uncapped) - expect(referrer.standardAwardValue.amount).toBe(STANDARD_AWARD_1Y.amount); - // awardPoolApproxValue capped at $1.50 (pool limit) - expect(referrer.awardPoolApproxValue.amount).toBe(poolAmount.amount); + // uncappedAwardValue = $2.50 (uncapped) + expect(referrer.uncappedAwardValue.amount).toBe(STANDARD_AWARD_1Y.amount); + // cappedAwardValue capped at $1.50 (pool limit) + expect(referrer.cappedAwardValue.amount).toBe(poolAmount.amount); // Pool fully depleted expect(result.aggregatedMetrics.awardPoolRemaining.amount).toBe(0n); }); @@ -194,15 +194,15 @@ describe("buildReferrerLeaderboardRevShareLimit", () => { const referrer = result.referrers.get(ADDR_A)!; expect(referrer.isQualified).toBe(true); - // standardAwardValue = 0.5 × (2 × $5) = $5.00 - expect(referrer.standardAwardValue.amount).toBe(parseUsdc("5").amount); - // awardPoolApproxValue = $2.50 (qualifying) + $2.50 (incremental) = $5.00 - expect(referrer.awardPoolApproxValue.amount).toBe(parseUsdc("5").amount); + // uncappedAwardValue = 0.5 × (2 × $5) = $5.00 + expect(referrer.uncappedAwardValue.amount).toBe(parseUsdc("5").amount); + // cappedAwardValue = $2.50 (qualifying) + $2.50 (incremental) = $5.00 + expect(referrer.cappedAwardValue.amount).toBe(parseUsdc("5").amount); }); }); describe("Scenario C-2 — already qualified, pool only partially covers incremental award", () => { - it("awardPoolApproxValue is partially truncated on subsequent event when pool is nearly empty", () => { + it("cappedAwardValue is partially truncated on subsequent event when pool is nearly empty", () => { // Pool = $3.00 // Event 1 at t=1000: 1 year → qualifies, claim min($2.50, $3.00) = $2.50, pool = $0.50 // Event 2 at t=2000: 1 year → already qualified, incremental $2.50, claim min($2.50, $0.50) = $0.50, pool = $0 @@ -216,10 +216,10 @@ describe("buildReferrerLeaderboardRevShareLimit", () => { const referrer = result.referrers.get(ADDR_A)!; expect(referrer.isQualified).toBe(true); - // standardAwardValue = 0.5 × $10 = $5.00 (uncapped) - expect(referrer.standardAwardValue.amount).toBe(parseUsdc("5").amount); - // awardPoolApproxValue = $2.50 + $0.50 = $3.00 (capped at pool) - expect(referrer.awardPoolApproxValue.amount).toBe(parseUsdc("3").amount); + // uncappedAwardValue = 0.5 × $10 = $5.00 (uncapped) + expect(referrer.uncappedAwardValue.amount).toBe(parseUsdc("5").amount); + // cappedAwardValue = $2.50 + $0.50 = $3.00 (capped at pool) + expect(referrer.cappedAwardValue.amount).toBe(parseUsdc("3").amount); // Pool fully depleted expect(result.aggregatedMetrics.awardPoolRemaining.amount).toBe(0n); }); @@ -235,8 +235,8 @@ describe("buildReferrerLeaderboardRevShareLimit", () => { const referrer = result.referrers.get(ADDR_A)!; expect(referrer.isQualified).toBe(true); - expect(referrer.standardAwardValue.amount).toBe(STANDARD_AWARD_1Y.amount); - expect(referrer.awardPoolApproxValue.amount).toBe(0n); + expect(referrer.uncappedAwardValue.amount).toBe(STANDARD_AWARD_1Y.amount); + expect(referrer.cappedAwardValue.amount).toBe(0n); }); }); @@ -256,16 +256,16 @@ describe("buildReferrerLeaderboardRevShareLimit", () => { const referrerB = result.referrers.get(ADDR_B)!; expect(referrerA.isQualified).toBe(true); - expect(referrerA.awardPoolApproxValue.amount).toBe(STANDARD_AWARD_1Y.amount); // $2.50 + expect(referrerA.cappedAwardValue.amount).toBe(STANDARD_AWARD_1Y.amount); // $2.50 expect(referrerB.isQualified).toBe(true); - expect(referrerB.awardPoolApproxValue.amount).toBe(parseUsdc("1.5").amount); // $1.50 (only remaining) + expect(referrerB.cappedAwardValue.amount).toBe(parseUsdc("1.5").amount); // $1.50 (only remaining) // Pool fully depleted expect(result.aggregatedMetrics.awardPoolRemaining.amount).toBe(0n); }); - it("referrer who qualifies after pool is empty gets $0 awardPoolApproxValue", () => { + it("referrer who qualifies after pool is empty gets $0 cappedAwardValue", () => { // Pool = $2.50 (only enough for 1 qualifying referrer) // ReferrerA qualifies at t=1000, claims $2.50, pool = $0 // ReferrerB qualifies at t=2000, claims min($2.50, $0) = $0 @@ -279,8 +279,8 @@ describe("buildReferrerLeaderboardRevShareLimit", () => { const referrerA = result.referrers.get(ADDR_A)!; const referrerB = result.referrers.get(ADDR_B)!; - expect(referrerA.awardPoolApproxValue.amount).toBe(STANDARD_AWARD_1Y.amount); // $2.50 - expect(referrerB.awardPoolApproxValue.amount).toBe(0n); // $0 — pool empty + expect(referrerA.cappedAwardValue.amount).toBe(STANDARD_AWARD_1Y.amount); // $2.50 + expect(referrerB.cappedAwardValue.amount).toBe(0n); // $0 — pool empty expect(result.aggregatedMetrics.awardPoolRemaining.amount).toBe(0n); }); @@ -302,12 +302,12 @@ describe("buildReferrerLeaderboardRevShareLimit", () => { const referrerC = result.referrers.get(ADDR_C)!; // Non-truncated: full standard award - expect(referrerA.awardPoolApproxValue.amount).toBe(STANDARD_AWARD_1Y.amount); + expect(referrerA.cappedAwardValue.amount).toBe(STANDARD_AWARD_1Y.amount); // Partially truncated: less than standard but > 0 - expect(referrerB.awardPoolApproxValue.amount).toBeGreaterThan(0n); - expect(referrerB.awardPoolApproxValue.amount).toBeLessThan(STANDARD_AWARD_1Y.amount); + expect(referrerB.cappedAwardValue.amount).toBeGreaterThan(0n); + expect(referrerB.cappedAwardValue.amount).toBeLessThan(STANDARD_AWARD_1Y.amount); // Fully truncated: pool empty - expect(referrerC.awardPoolApproxValue.amount).toBe(0n); + expect(referrerC.cappedAwardValue.amount).toBe(0n); expect(result.aggregatedMetrics.awardPoolRemaining.amount).toBe(0n); }); }); @@ -329,15 +329,13 @@ describe("buildReferrerLeaderboardRevShareLimit", () => { const result = buildReferrerLeaderboardRevShareLimit(events, rules, accurateAsOf); // ADDR_A has the lower (earlier) id, should claim the pool first - expect(result.referrers.get(ADDR_A)!.awardPoolApproxValue.amount).toBe( - STANDARD_AWARD_1Y.amount, - ); - expect(result.referrers.get(ADDR_B)!.awardPoolApproxValue.amount).toBe(0n); + expect(result.referrers.get(ADDR_A)!.cappedAwardValue.amount).toBe(STANDARD_AWARD_1Y.amount); + expect(result.referrers.get(ADDR_B)!.cappedAwardValue.amount).toBe(0n); }); }); describe("Ranking", () => { - it("ranks referrers by qualifiedAwardValue desc, then standardAwardValue desc", () => { + it("ranks referrers by qualifiedAwardValue desc, then uncappedAwardValue desc", () => { // Pool = $1000 (unlimited for this test) // ADDR_A: 1 year → qualifies at t=1000, qualifiedAward = $2.50, standardAward = $2.50 // ADDR_B: 2 years → qualifies at t=2000, qualifiedAward = $5.00, standardAward = $5.00 @@ -359,7 +357,7 @@ describe("buildReferrerLeaderboardRevShareLimit", () => { expect(result.referrers.get(ADDR_C)!.rank).toBe(3); }); - it("two fully-truncated referrers are ranked by standardAwardValue desc", () => { + it("two fully-truncated referrers are ranked by uncappedAwardValue desc", () => { // Pool = $0 — nobody gets pool money // ADDR_A: 2 years → qualifies, standardAward = $5.00, qualifiedAward = $0 // ADDR_B: 1 year → qualifies, standardAward = $2.50, qualifiedAward = $0 @@ -438,14 +436,14 @@ describe("buildReferrerLeaderboardRevShareLimit", () => { expect(referrerA.isQualified).toBe(true); expect(referrerA.isAdminDisqualified).toBe(false); expect(referrerA.adminDisqualificationReason).toBe(null); - expect(referrerA.awardPoolApproxValue.amount).toBe(STANDARD_AWARD_1Y.amount); + expect(referrerA.cappedAwardValue.amount).toBe(STANDARD_AWARD_1Y.amount); expect(referrerB.isQualified).toBe(true); expect(referrerB.isAdminDisqualified).toBe(false); expect(referrerB.adminDisqualificationReason).toBe(null); }); - it("disqualified referrer who met threshold: awardPoolApproxValue = 0, pool preserved for next", () => { + it("disqualified referrer who met threshold: cappedAwardValue = 0, pool preserved for next", () => { // ADDR_A qualifies by revenue but is admin-disqualified → pool claim = 0 // ADDR_B qualifies later → gets the full pool share const rules = buildTestRules(parseUsdc("1000"), parseUsdc("5"), [ @@ -463,12 +461,12 @@ describe("buildReferrerLeaderboardRevShareLimit", () => { expect(referrerA.isAdminDisqualified).toBe(true); expect(referrerA.adminDisqualificationReason).toBe("self-referral"); expect(referrerA.isQualified).toBe(false); - expect(referrerA.awardPoolApproxValue.amount).toBe(0n); + expect(referrerA.cappedAwardValue.amount).toBe(0n); // Pool was not consumed by ADDR_A, so ADDR_B gets the full award expect(referrerB.isQualified).toBe(true); expect(referrerB.isAdminDisqualified).toBe(false); - expect(referrerB.awardPoolApproxValue.amount).toBe(STANDARD_AWARD_1Y.amount); + expect(referrerB.cappedAwardValue.amount).toBe(STANDARD_AWARD_1Y.amount); }); it("disqualified referrer who never met the revenue threshold: pool unchanged", () => { @@ -484,7 +482,7 @@ describe("buildReferrerLeaderboardRevShareLimit", () => { expect(referrerA.isAdminDisqualified).toBe(true); expect(referrerA.adminDisqualificationReason).toBe("promoting discounts"); expect(referrerA.isQualified).toBe(false); - expect(referrerA.awardPoolApproxValue.amount).toBe(0n); + expect(referrerA.cappedAwardValue.amount).toBe(0n); // Pool fully intact expect(result.aggregatedMetrics.awardPoolRemaining.amount).toBe(parseUsdc("1000").amount); }); @@ -515,18 +513,18 @@ describe("buildReferrerLeaderboardRevShareLimit", () => { expect(referrerB.rank).toBe(1); expect(referrerB.isQualified).toBe(true); expect(referrerB.isAdminDisqualified).toBe(false); - expect(referrerB.awardPoolApproxValue.amount).toBe(STANDARD_AWARD_1Y.amount); + expect(referrerB.cappedAwardValue.amount).toBe(STANDARD_AWARD_1Y.amount); expect(referrerA.rank).toBe(2); expect(referrerA.isAdminDisqualified).toBe(true); expect(referrerA.adminDisqualificationReason).toBe("cheating"); expect(referrerA.isQualified).toBe(false); - expect(referrerA.awardPoolApproxValue.amount).toBe(0n); + expect(referrerA.cappedAwardValue.amount).toBe(0n); expect(referrerC.rank).toBe(3); expect(referrerC.isQualified).toBe(false); expect(referrerC.isAdminDisqualified).toBe(false); - expect(referrerC.awardPoolApproxValue.amount).toBe(0n); + expect(referrerC.cappedAwardValue.amount).toBe(0n); }); it("multiple disqualifications: all disqualified referrers get isAdminDisqualified=true", () => { @@ -548,17 +546,17 @@ describe("buildReferrerLeaderboardRevShareLimit", () => { expect(referrerA.isAdminDisqualified).toBe(true); expect(referrerA.adminDisqualificationReason).toBe("reason-a"); expect(referrerA.isQualified).toBe(false); - expect(referrerA.awardPoolApproxValue.amount).toBe(0n); + expect(referrerA.cappedAwardValue.amount).toBe(0n); expect(referrerB.isAdminDisqualified).toBe(true); expect(referrerB.adminDisqualificationReason).toBe("reason-b"); expect(referrerB.isQualified).toBe(false); - expect(referrerB.awardPoolApproxValue.amount).toBe(0n); + expect(referrerB.cappedAwardValue.amount).toBe(0n); expect(referrerC.isAdminDisqualified).toBe(false); expect(referrerC.adminDisqualificationReason).toBe(null); expect(referrerC.isQualified).toBe(true); - expect(referrerC.awardPoolApproxValue.amount).toBe(STANDARD_AWARD_1Y.amount); + expect(referrerC.cappedAwardValue.amount).toBe(STANDARD_AWARD_1Y.amount); }); it("duplicate address in disqualifications: buildReferralProgramRulesRevShareLimit throws", () => { diff --git a/packages/ens-referrals/src/v1/award-models/rev-share-limit/leaderboard.ts b/packages/ens-referrals/src/v1/award-models/rev-share-limit/leaderboard.ts index 41c79cd1f1..bea04a39da 100644 --- a/packages/ens-referrals/src/v1/award-models/rev-share-limit/leaderboard.ts +++ b/packages/ens-referrals/src/v1/award-models/rev-share-limit/leaderboard.ts @@ -146,7 +146,7 @@ export const buildReferrerLeaderboardRevShareLimit = ( // Compute from aggregated totals to match the single-division used in final output. const accumulatedStandardAwardAmount = scalePrice( priceUsdc(totalBaseRevenueAmount), - rules.qualifiedRevenueShare, + rules.maxBaseRevenueShare, ).amount; const claimAmount = accumulatedStandardAwardAmount < poolRemainingAmount @@ -162,7 +162,7 @@ export const buildReferrerLeaderboardRevShareLimit = ( BigInt(SECONDS_PER_YEAR); const incrementalStandardAwardAmount = scalePrice( priceUsdc(incrementalBaseRevenueAmount), - rules.qualifiedRevenueShare, + rules.maxBaseRevenueShare, ).amount; const claimAmount = incrementalStandardAwardAmount < poolRemainingAmount @@ -175,7 +175,7 @@ export const buildReferrerLeaderboardRevShareLimit = ( } // 3. Sort referrers to assign ranks: - // 1. qualifiedAwardValue (awardPoolApproxValue) desc — actual pool claims, race winners first + // 1. qualifiedAwardValue (cappedAwardValue) desc — actual pool claims, race winners first // 2. totalIncrementalDuration desc — tie-break for pool-depleted referrers // 3. referrer address desc — deterministic tie-break // Both `a` and `b` are keys from `referrerStates`, so lookups are always defined. @@ -221,14 +221,14 @@ export const buildReferrerLeaderboardRevShareLimit = ( rules, ); - const standardAwardValue = scalePrice( + const uncappedAwardValue = scalePrice( revShareMetrics.totalBaseRevenueContribution, - rules.qualifiedRevenueShare, + rules.maxBaseRevenueShare, ); return buildAwardedReferrerMetricsRevShareLimit( rankedMetrics, - standardAwardValue, + uncappedAwardValue, priceUsdc(state.qualifiedAwardValueAmount), rules, ); diff --git a/packages/ens-referrals/src/v1/award-models/rev-share-limit/metrics.ts b/packages/ens-referrals/src/v1/award-models/rev-share-limit/metrics.ts index b5d0d65719..6684f9a75a 100644 --- a/packages/ens-referrals/src/v1/award-models/rev-share-limit/metrics.ts +++ b/packages/ens-referrals/src/v1/award-models/rev-share-limit/metrics.ts @@ -73,7 +73,7 @@ export interface RankedReferrerMetricsRevShareLimit extends ReferrerMetricsRevSh * Identifies if the referrer meets the qualifications of the {@link ReferralProgramRulesRevShareLimit} to receive a non-zero `awardPoolShare`. * * @invariant true if and only if `totalBaseRevenueContribution` is greater than or equal to - * {@link ReferralProgramRulesRevShareLimit.minQualifiedRevenueContribution} AND + * {@link ReferralProgramRulesRevShareLimit.minBaseRevenueContribution} AND * {@link isAdminDisqualified} is false. */ isQualified: boolean; @@ -159,12 +159,12 @@ export const buildRankedReferrerMetricsRevShareLimit = ( export interface AwardedReferrerMetricsRevShareLimit extends RankedReferrerMetricsRevShareLimit { /** * The standard (uncapped) USDC award value for this referrer, computed as - * `qualifiedRevenueShare × totalBaseRevenueContribution`. + * `maxBaseRevenueShare × totalBaseRevenueContribution`. * * Represents what the referrer would receive if the pool were unlimited and the referrer were qualified. * Independent of the pool state and qualification status. */ - standardAwardValue: PriceUsdc; + uncappedAwardValue: PriceUsdc; /** * The approximate USDC value of the referrer's award. @@ -173,10 +173,10 @@ export interface AwardedReferrerMetricsRevShareLimit extends RankedReferrerMetri * the remaining pool at the time of their qualifying events. * * @invariant Guaranteed to be a valid PriceUsdc with amount between 0 and {@link ReferralProgramRulesRevShareLimit.totalAwardPoolValue.amount} (inclusive) - * @invariant Always <= standardAwardValue.amount + * @invariant Always <= uncappedAwardValue.amount * @invariant Amount equal to 0 when {@link isAdminDisqualified} is true. */ - awardPoolApproxValue: PriceUsdc; + cappedAwardValue: PriceUsdc; } export const validateAwardedReferrerMetricsRevShareLimit = ( @@ -185,43 +185,43 @@ export const validateAwardedReferrerMetricsRevShareLimit = ( ): void => { validateRankedReferrerMetricsRevShareLimit(metrics, rules); - makePriceUsdcSchema("AwardedReferrerMetricsRevShareLimit.standardAwardValue").parse( - metrics.standardAwardValue, + makePriceUsdcSchema("AwardedReferrerMetricsRevShareLimit.uncappedAwardValue").parse( + metrics.uncappedAwardValue, ); - makePriceUsdcSchema("AwardedReferrerMetricsRevShareLimit.awardPoolApproxValue").parse( - metrics.awardPoolApproxValue, + makePriceUsdcSchema("AwardedReferrerMetricsRevShareLimit.cappedAwardValue").parse( + metrics.cappedAwardValue, ); - if (metrics.isAdminDisqualified && metrics.awardPoolApproxValue.amount !== 0n) { + if (metrics.isAdminDisqualified && metrics.cappedAwardValue.amount !== 0n) { throw new Error( - `AwardedReferrerMetricsRevShareLimit: awardPoolApproxValue.amount must be 0n for admin-disqualified referrers, got ${metrics.awardPoolApproxValue.amount.toString()}.`, + `AwardedReferrerMetricsRevShareLimit: cappedAwardValue.amount must be 0n for admin-disqualified referrers, got ${metrics.cappedAwardValue.amount.toString()}.`, ); } - if (metrics.awardPoolApproxValue.amount > rules.totalAwardPoolValue.amount) { + if (metrics.cappedAwardValue.amount > rules.totalAwardPoolValue.amount) { throw new Error( - `AwardedReferrerMetricsRevShareLimit: awardPoolApproxValue.amount ${metrics.awardPoolApproxValue.amount.toString()} exceeds totalAwardPoolValue.amount ${rules.totalAwardPoolValue.amount.toString()}.`, + `AwardedReferrerMetricsRevShareLimit: cappedAwardValue.amount ${metrics.cappedAwardValue.amount.toString()} exceeds totalAwardPoolValue.amount ${rules.totalAwardPoolValue.amount.toString()}.`, ); } - if (metrics.awardPoolApproxValue.amount > metrics.standardAwardValue.amount) { + if (metrics.cappedAwardValue.amount > metrics.uncappedAwardValue.amount) { throw new Error( - `AwardedReferrerMetricsRevShareLimit: awardPoolApproxValue.amount ${metrics.awardPoolApproxValue.amount.toString()} exceeds standardAwardValue.amount ${metrics.standardAwardValue.amount.toString()}.`, + `AwardedReferrerMetricsRevShareLimit: cappedAwardValue.amount ${metrics.cappedAwardValue.amount.toString()} exceeds uncappedAwardValue.amount ${metrics.uncappedAwardValue.amount.toString()}.`, ); } }; export const buildAwardedReferrerMetricsRevShareLimit = ( referrer: RankedReferrerMetricsRevShareLimit, - standardAwardValue: PriceUsdc, - awardPoolApproxValue: PriceUsdc, + uncappedAwardValue: PriceUsdc, + cappedAwardValue: PriceUsdc, rules: ReferralProgramRulesRevShareLimit, ): AwardedReferrerMetricsRevShareLimit => { const result = { ...referrer, - standardAwardValue, - awardPoolApproxValue, + uncappedAwardValue, + cappedAwardValue, } satisfies AwardedReferrerMetricsRevShareLimit; validateAwardedReferrerMetricsRevShareLimit(result, rules); @@ -307,21 +307,21 @@ export const validateUnrankedReferrerMetricsRevShareLimit = ( ); } - makePriceUsdcSchema("UnrankedReferrerMetricsRevShareLimit.standardAwardValue").parse( - metrics.standardAwardValue, + makePriceUsdcSchema("UnrankedReferrerMetricsRevShareLimit.uncappedAwardValue").parse( + metrics.uncappedAwardValue, ); - if (metrics.standardAwardValue.amount !== 0n) { + if (metrics.uncappedAwardValue.amount !== 0n) { throw new Error( - `Invalid UnrankedReferrerMetricsRevShareLimit: standardAwardValue.amount must be 0n, got: ${metrics.standardAwardValue.amount.toString()}.`, + `Invalid UnrankedReferrerMetricsRevShareLimit: uncappedAwardValue.amount must be 0n, got: ${metrics.uncappedAwardValue.amount.toString()}.`, ); } - makePriceUsdcSchema("UnrankedReferrerMetricsRevShareLimit.awardPoolApproxValue").parse( - metrics.awardPoolApproxValue, + makePriceUsdcSchema("UnrankedReferrerMetricsRevShareLimit.cappedAwardValue").parse( + metrics.cappedAwardValue, ); - if (metrics.awardPoolApproxValue.amount !== 0n) { + if (metrics.cappedAwardValue.amount !== 0n) { throw new Error( - `Invalid UnrankedReferrerMetricsRevShareLimit: awardPoolApproxValue.amount must be 0n, got: ${metrics.awardPoolApproxValue.amount.toString()}.`, + `Invalid UnrankedReferrerMetricsRevShareLimit: cappedAwardValue.amount must be 0n, got: ${metrics.cappedAwardValue.amount.toString()}.`, ); } }; @@ -343,8 +343,8 @@ export const buildUnrankedReferrerMetricsRevShareLimit = ( totalBaseRevenueContribution: priceUsdc(0n), rank: null, isQualified: false, - standardAwardValue: priceUsdc(0n), - awardPoolApproxValue: priceUsdc(0n), + uncappedAwardValue: priceUsdc(0n), + cappedAwardValue: priceUsdc(0n), isAdminDisqualified: disqualification !== null, adminDisqualificationReason: disqualification?.reason ?? null, } satisfies UnrankedReferrerMetricsRevShareLimit; diff --git a/packages/ens-referrals/src/v1/award-models/rev-share-limit/rules.ts b/packages/ens-referrals/src/v1/award-models/rev-share-limit/rules.ts index 9f75014889..6f705401d5 100644 --- a/packages/ens-referrals/src/v1/award-models/rev-share-limit/rules.ts +++ b/packages/ens-referrals/src/v1/award-models/rev-share-limit/rules.ts @@ -60,14 +60,14 @@ export interface ReferralProgramRulesRevShareLimit extends BaseReferralProgramRu /** * The minimum base revenue contribution required for a referrer to qualify. */ - minQualifiedRevenueContribution: PriceUsdc; + minBaseRevenueContribution: PriceUsdc; /** * The fraction of the referrer's base revenue contribution that constitutes their potential award. * * @invariant Guaranteed to be a number between 0 and 1 (inclusive) */ - qualifiedRevenueShare: number; + maxBaseRevenueShare: number; /** * Admin-imposed disqualifications for this edition. @@ -85,17 +85,17 @@ export const validateReferralProgramRulesRevShareLimit = ( rules.totalAwardPoolValue, ); - makePriceUsdcSchema("ReferralProgramRulesRevShareLimit.minQualifiedRevenueContribution").parse( - rules.minQualifiedRevenueContribution, + makePriceUsdcSchema("ReferralProgramRulesRevShareLimit.minBaseRevenueContribution").parse( + rules.minBaseRevenueContribution, ); if ( - !Number.isFinite(rules.qualifiedRevenueShare) || - rules.qualifiedRevenueShare < 0 || - rules.qualifiedRevenueShare > 1 + !Number.isFinite(rules.maxBaseRevenueShare) || + rules.maxBaseRevenueShare < 0 || + rules.maxBaseRevenueShare > 1 ) { throw new Error( - `ReferralProgramRulesRevShareLimit: qualifiedRevenueShare must be between 0 and 1 (inclusive), got ${rules.qualifiedRevenueShare}.`, + `ReferralProgramRulesRevShareLimit: maxBaseRevenueShare must be between 0 and 1 (inclusive), got ${rules.maxBaseRevenueShare}.`, ); } @@ -121,8 +121,8 @@ export const validateReferralProgramRulesRevShareLimit = ( export const buildReferralProgramRulesRevShareLimit = ( totalAwardPoolValue: PriceUsdc, - minQualifiedRevenueContribution: PriceUsdc, - qualifiedRevenueShare: number, + minBaseRevenueContribution: PriceUsdc, + maxBaseRevenueShare: number, startTime: UnixTimestamp, endTime: UnixTimestamp, subregistryId: AccountId, @@ -133,8 +133,8 @@ export const buildReferralProgramRulesRevShareLimit = ( const result = { awardModel: ReferralProgramAwardModels.RevShareLimit, totalAwardPoolValue, - minQualifiedRevenueContribution, - qualifiedRevenueShare, + minBaseRevenueContribution, + maxBaseRevenueShare, startTime, endTime, subregistryId, @@ -167,7 +167,7 @@ export function isReferrerQualifiedRevShareLimit( (d) => d.referrer === normalizedReferrer, ); return ( - totalBaseRevenueContribution.amount >= rules.minQualifiedRevenueContribution.amount && + totalBaseRevenueContribution.amount >= rules.minBaseRevenueContribution.amount && !isAdminDisqualified ); } From ae807a6d4dc51d09025b9fe629f7a88734ed4598 Mon Sep 17 00:00:00 2001 From: Goader Date: Mon, 6 Apr 2026 17:48:54 +0200 Subject: [PATCH 02/11] update schema to ensure unranked get 0 awards --- .../award-models/rev-share-limit/api/zod-schemas.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/ens-referrals/src/v1/award-models/rev-share-limit/api/zod-schemas.ts b/packages/ens-referrals/src/v1/award-models/rev-share-limit/api/zod-schemas.ts index cab71a8158..56856d6a8e 100644 --- a/packages/ens-referrals/src/v1/award-models/rev-share-limit/api/zod-schemas.ts +++ b/packages/ens-referrals/src/v1/award-models/rev-share-limit/api/zod-schemas.ts @@ -132,10 +132,13 @@ export const makeUnrankedReferrerMetricsRevShareLimitSchema = ( .min(1, `${valueLabel}.adminDisqualificationReason must not be empty`) .nullable(), }) - .refine((data) => data.cappedAwardValue.amount <= data.uncappedAwardValue.amount, { - message: `${valueLabel}.cappedAwardValue must be <= ${valueLabel}.uncappedAwardValue`, - path: ["cappedAwardValue"], - }) + .refine( + (data) => data.uncappedAwardValue.amount === 0n && data.cappedAwardValue.amount === 0n, + { + message: `${valueLabel}.uncappedAwardValue and cappedAwardValue must both be 0 for unranked referrers`, + path: ["uncappedAwardValue", "cappedAwardValue"], + }, + ) .refine((data) => data.isAdminDisqualified === (data.adminDisqualificationReason !== null), { message: `${valueLabel}.adminDisqualificationReason must be non-null iff isAdminDisqualified is true`, path: ["adminDisqualificationReason"], From 1443e646838ffbfbeb79348159939e16072605c6 Mon Sep 17 00:00:00 2001 From: Goader Date: Mon, 6 Apr 2026 18:19:01 +0200 Subject: [PATCH 03/11] more invariants validation --- .../src/v1/award-models/rev-share-limit/api/zod-schemas.ts | 4 ++++ .../src/v1/award-models/rev-share-limit/metrics.ts | 7 +++++++ 2 files changed, 11 insertions(+) diff --git a/packages/ens-referrals/src/v1/award-models/rev-share-limit/api/zod-schemas.ts b/packages/ens-referrals/src/v1/award-models/rev-share-limit/api/zod-schemas.ts index 56856d6a8e..10918d9c33 100644 --- a/packages/ens-referrals/src/v1/award-models/rev-share-limit/api/zod-schemas.ts +++ b/packages/ens-referrals/src/v1/award-models/rev-share-limit/api/zod-schemas.ts @@ -104,6 +104,10 @@ export const makeAwardedReferrerMetricsRevShareLimitSchema = ( .refine((data) => data.isAdminDisqualified === (data.adminDisqualificationReason !== null), { message: `${valueLabel}.adminDisqualificationReason must be non-null iff isAdminDisqualified is true`, path: ["adminDisqualificationReason"], + }) + .refine((data) => data.isQualified || data.cappedAwardValue.amount === 0n, { + message: `${valueLabel}.cappedAwardValue must be 0 when isQualified is false`, + path: ["cappedAwardValue"], }); /** diff --git a/packages/ens-referrals/src/v1/award-models/rev-share-limit/metrics.ts b/packages/ens-referrals/src/v1/award-models/rev-share-limit/metrics.ts index 6684f9a75a..3f7f64a4d7 100644 --- a/packages/ens-referrals/src/v1/award-models/rev-share-limit/metrics.ts +++ b/packages/ens-referrals/src/v1/award-models/rev-share-limit/metrics.ts @@ -175,6 +175,7 @@ export interface AwardedReferrerMetricsRevShareLimit extends RankedReferrerMetri * @invariant Guaranteed to be a valid PriceUsdc with amount between 0 and {@link ReferralProgramRulesRevShareLimit.totalAwardPoolValue.amount} (inclusive) * @invariant Always <= uncappedAwardValue.amount * @invariant Amount equal to 0 when {@link isAdminDisqualified} is true. + * @invariant Amount equal to 0 when {@link isQualified} is false. */ cappedAwardValue: PriceUsdc; } @@ -199,6 +200,12 @@ export const validateAwardedReferrerMetricsRevShareLimit = ( ); } + if (!metrics.isQualified && metrics.cappedAwardValue.amount !== 0n) { + throw new Error( + `AwardedReferrerMetricsRevShareLimit: cappedAwardValue.amount must be 0n for unqualified referrers, got ${metrics.cappedAwardValue.amount.toString()}.`, + ); + } + if (metrics.cappedAwardValue.amount > rules.totalAwardPoolValue.amount) { throw new Error( `AwardedReferrerMetricsRevShareLimit: cappedAwardValue.amount ${metrics.cappedAwardValue.amount.toString()} exceeds totalAwardPoolValue.amount ${rules.totalAwardPoolValue.amount.toString()}.`, From fb767c295dd675449b2402475b40680e8157ccaa Mon Sep 17 00:00:00 2001 From: Goader Date: Mon, 6 Apr 2026 20:28:00 +0200 Subject: [PATCH 04/11] review applied --- .../rev-share-limit/api/zod-schemas.ts | 15 ++++++++------- .../v1/award-models/rev-share-limit/metrics.ts | 2 +- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/ens-referrals/src/v1/award-models/rev-share-limit/api/zod-schemas.ts b/packages/ens-referrals/src/v1/award-models/rev-share-limit/api/zod-schemas.ts index 10918d9c33..fbda2cf37d 100644 --- a/packages/ens-referrals/src/v1/award-models/rev-share-limit/api/zod-schemas.ts +++ b/packages/ens-referrals/src/v1/award-models/rev-share-limit/api/zod-schemas.ts @@ -136,13 +136,14 @@ export const makeUnrankedReferrerMetricsRevShareLimitSchema = ( .min(1, `${valueLabel}.adminDisqualificationReason must not be empty`) .nullable(), }) - .refine( - (data) => data.uncappedAwardValue.amount === 0n && data.cappedAwardValue.amount === 0n, - { - message: `${valueLabel}.uncappedAwardValue and cappedAwardValue must both be 0 for unranked referrers`, - path: ["uncappedAwardValue", "cappedAwardValue"], - }, - ) + .refine((data) => data.uncappedAwardValue.amount === 0n, { + message: `${valueLabel}.uncappedAwardValue must be 0 for unranked referrers`, + path: ["uncappedAwardValue"], + }) + .refine((data) => data.cappedAwardValue.amount === 0n, { + message: `${valueLabel}.cappedAwardValue must be 0 for unranked referrers`, + path: ["cappedAwardValue"], + }) .refine((data) => data.isAdminDisqualified === (data.adminDisqualificationReason !== null), { message: `${valueLabel}.adminDisqualificationReason must be non-null iff isAdminDisqualified is true`, path: ["adminDisqualificationReason"], diff --git a/packages/ens-referrals/src/v1/award-models/rev-share-limit/metrics.ts b/packages/ens-referrals/src/v1/award-models/rev-share-limit/metrics.ts index 3f7f64a4d7..af120e8857 100644 --- a/packages/ens-referrals/src/v1/award-models/rev-share-limit/metrics.ts +++ b/packages/ens-referrals/src/v1/award-models/rev-share-limit/metrics.ts @@ -70,7 +70,7 @@ export interface RankedReferrerMetricsRevShareLimit extends ReferrerMetricsRevSh rank: ReferrerRank; /** - * Identifies if the referrer meets the qualifications of the {@link ReferralProgramRulesRevShareLimit} to receive a non-zero `awardPoolShare`. + * Identifies if the referrer meets the qualifications of the {@link ReferralProgramRulesRevShareLimit} to receive a non-zero `cappedAwardValue`. * * @invariant true if and only if `totalBaseRevenueContribution` is greater than or equal to * {@link ReferralProgramRulesRevShareLimit.minBaseRevenueContribution} AND From ca514bae27770971ecbdb5d3eec6b3a56aa31184 Mon Sep 17 00:00:00 2001 From: Goader Date: Tue, 7 Apr 2026 17:00:01 +0200 Subject: [PATCH 05/11] review applied --- .changeset/bright-foxes-dance.md | 2 +- .../referrer-leaderboard/mocks-v1.ts | 6 +- packages/ens-referrals/README.md | 2 +- .../src/v1/api/zod-schemas.test.ts | 28 +++++---- .../award-models/pie-split/api/serialize.ts | 2 +- .../pie-split/api/serialized-types.ts | 4 +- .../award-models/pie-split/api/zod-schemas.ts | 2 +- .../src/v1/award-models/pie-split/metrics.ts | 14 ++--- .../src/v1/award-models/pie-split/rules.ts | 12 ++-- .../rev-share-limit/api/serialize.ts | 3 +- .../rev-share-limit/api/serialized-types.ts | 5 +- .../rev-share-limit/api/zod-schemas.ts | 21 ++++++- .../rev-share-limit/leaderboard.test.ts | 15 +++-- .../rev-share-limit/leaderboard.ts | 58 ++++++++----------- .../award-models/rev-share-limit/metrics.ts | 35 ++++++----- .../v1/award-models/rev-share-limit/rules.ts | 50 ++++++++-------- .../ens-referrals/src/v1/edition-defaults.ts | 1 + .../src/v1/leaderboard-page.test.ts | 4 +- 18 files changed, 140 insertions(+), 124 deletions(-) diff --git a/.changeset/bright-foxes-dance.md b/.changeset/bright-foxes-dance.md index 36ba8b0aa6..73c5896e8c 100644 --- a/.changeset/bright-foxes-dance.md +++ b/.changeset/bright-foxes-dance.md @@ -2,4 +2,4 @@ "@namehash/ens-referrals": minor --- -Rename rev-share-limit API fields for clarity: `minQualifiedRevenueContribution` → `minBaseRevenueContribution`, `qualifiedRevenueShare` → `maxBaseRevenueShare`, `standardAwardValue` → `uncappedAwardValue`, `awardPoolApproxValue` → `cappedAwardValue`. +Rename rev-share-limit API fields for clarity: `minQualifiedRevenueContribution` → `minBaseRevenueContribution`, `qualifiedRevenueShare` → `maxBaseRevenueShare`, `standardAwardValue` → `uncappedAwardValue`, `awardPoolApproxValue` → `cappedAwardValue`. Rename `totalAwardPoolValue` → `awardPool` for both rev-share-limit and pie-split rules. Extract the previously hardcoded `BASE_REVENUE_CONTRIBUTION_PER_YEAR` constant into a per-edition `baseAnnualRevenueContribution` rule field. diff --git a/apps/ensapi/src/lib/ensanalytics/referrer-leaderboard/mocks-v1.ts b/apps/ensapi/src/lib/ensanalytics/referrer-leaderboard/mocks-v1.ts index 02de034428..3e5bf2f0fb 100644 --- a/apps/ensapi/src/lib/ensanalytics/referrer-leaderboard/mocks-v1.ts +++ b/apps/ensapi/src/lib/ensanalytics/referrer-leaderboard/mocks-v1.ts @@ -179,7 +179,7 @@ export const emptyReferralLeaderboard: ReferrerLeaderboardPieSplit = { awardModel: ReferralProgramAwardModels.PieSplit, rules: { awardModel: ReferralProgramAwardModels.PieSplit, - totalAwardPoolValue: parseUsdc("10000"), + awardPool: parseUsdc("10000"), maxQualifiedReferrers: 10, startTime: 1735689600, endTime: 1767225599, @@ -205,7 +205,7 @@ export const populatedReferrerLeaderboard: ReferrerLeaderboardPieSplit = { awardModel: ReferralProgramAwardModels.PieSplit, rules: { awardModel: ReferralProgramAwardModels.PieSplit, - totalAwardPoolValue: parseUsdc("10000"), + awardPool: parseUsdc("10000"), maxQualifiedReferrers: 10, startTime: 1735689600, endTime: 1767225599, @@ -698,7 +698,7 @@ export const referrerLeaderboardPageResponseOk = { awardModel: ReferralProgramAwardModels.PieSplit, rules: { awardModel: ReferralProgramAwardModels.PieSplit, - totalAwardPoolValue: parseUsdc("10000"), + awardPool: parseUsdc("10000"), maxQualifiedReferrers: 10, startTime: 1735689600, endTime: 1767225599, diff --git a/packages/ens-referrals/README.md b/packages/ens-referrals/README.md index 6cb6494dba..ea2cf9860e 100644 --- a/packages/ens-referrals/README.md +++ b/packages/ens-referrals/README.md @@ -102,7 +102,7 @@ if (response.responseCode === ReferrerLeaderboardPageResponseCodes.Ok) { ); console.log(`Max Base Revenue Share: ${leaderboardPage.rules.maxBaseRevenueShare}`); console.log( - `Tentative award for the best referrer: ${firstReferrer !== null ? firstReferrer.cappedAwardValue : noReferrersFallback}`, + `Tentative award for the top ranked referrer: ${firstReferrer !== null ? firstReferrer.cappedAwardValue : noReferrersFallback}`, ); } } diff --git a/packages/ens-referrals/src/v1/api/zod-schemas.test.ts b/packages/ens-referrals/src/v1/api/zod-schemas.test.ts index cb79b93efc..f93e0f87bc 100644 --- a/packages/ens-referrals/src/v1/api/zod-schemas.test.ts +++ b/packages/ens-referrals/src/v1/api/zod-schemas.test.ts @@ -28,7 +28,7 @@ describe("makeReferralProgramEditionConfigSetArraySchema", () => { displayName: "December 2025", rules: { awardModel: ReferralProgramAwardModels.PieSplit, - totalAwardPoolValue: parseUsdc("1000"), + awardPool: parseUsdc("1000"), maxQualifiedReferrers: 100, startTime: 1000000, endTime: 2000000, @@ -43,8 +43,9 @@ describe("makeReferralProgramEditionConfigSetArraySchema", () => { displayName: "January 2026", rules: { awardModel: ReferralProgramAwardModels.RevShareLimit, - totalAwardPoolValue: parseUsdc("500"), + awardPool: parseUsdc("500"), minBaseRevenueContribution: parseUsdc("10"), + baseAnnualRevenueContribution: parseUsdc("5"), maxBaseRevenueShare: 0.5, startTime: 1000000, endTime: 2000000, @@ -92,12 +93,14 @@ describe("makeReferralProgramEditionConfigSetArraySchema", () => { const rules = revShareLimit!.rules as { awardModel: typeof ReferralProgramAwardModels.RevShareLimit; - totalAwardPoolValue: { amount: bigint; currency: string }; + awardPool: { amount: bigint; currency: string }; minBaseRevenueContribution: { amount: bigint; currency: string }; + baseAnnualRevenueContribution: { amount: bigint; currency: string }; maxBaseRevenueShare: number; }; - expect(rules.totalAwardPoolValue).toBeDefined(); + expect(rules.awardPool).toBeDefined(); expect(rules.minBaseRevenueContribution).toBeDefined(); + expect(rules.baseAnnualRevenueContribution).toBeDefined(); expect(typeof rules.maxBaseRevenueShare).toBe("number"); expect(rules.maxBaseRevenueShare).toBe(0.5); expect(revShareLimit!.rules.areAwardsDistributed).toBe( @@ -189,7 +192,7 @@ describe("makeReferrerLeaderboardPageSchema", () => { awardModel: ReferralProgramAwardModels.PieSplit, rules: { awardModel: ReferralProgramAwardModels.PieSplit, - totalAwardPoolValue: parseUsdc("1000"), + awardPool: parseUsdc("1000"), maxQualifiedReferrers: 100, startTime: 1000000, endTime: 2000000, @@ -214,8 +217,9 @@ describe("makeReferrerLeaderboardPageSchema", () => { awardModel: ReferralProgramAwardModels.RevShareLimit, rules: { awardModel: ReferralProgramAwardModels.RevShareLimit, - totalAwardPoolValue: parseUsdc("2000"), + awardPool: parseUsdc("2000"), minBaseRevenueContribution: parseUsdc("10"), + baseAnnualRevenueContribution: parseUsdc("5"), maxBaseRevenueShare: 0.5, startTime: 1000000, endTime: 2000000, @@ -275,7 +279,7 @@ describe("makeReferrerLeaderboardPageSchema", () => { ...pieSplitLeaderboardPage, rules: { ...pieSplitLeaderboardPage.rules, - totalAwardPoolValue: { amount: "not-a-number", currency: CurrencyIds.USDC }, + awardPool: { amount: "not-a-number", currency: CurrencyIds.USDC }, }, }; @@ -307,7 +311,7 @@ describe("makeReferralProgramEditionSummarySchema", () => { status: ReferralProgramEditionStatuses.Active, rules: { awardModel: ReferralProgramAwardModels.PieSplit, - totalAwardPoolValue: parseUsdc("1000"), + awardPool: parseUsdc("1000"), maxQualifiedReferrers: 100, startTime: 1000000, endTime: 2000000, @@ -324,8 +328,9 @@ describe("makeReferralProgramEditionSummarySchema", () => { status: ReferralProgramEditionStatuses.Active, rules: { awardModel: ReferralProgramAwardModels.RevShareLimit, - totalAwardPoolValue: parseUsdc("2000"), + awardPool: parseUsdc("2000"), minBaseRevenueContribution: parseUsdc("10"), + baseAnnualRevenueContribution: parseUsdc("5"), maxBaseRevenueShare: 0.5, startTime: 1000000, endTime: 2000000, @@ -423,7 +428,7 @@ describe("makeReferrerEditionMetricsSchema", () => { const pieSplitRules = { awardModel: ReferralProgramAwardModels.PieSplit, - totalAwardPoolValue: parseUsdc("1000"), + awardPool: parseUsdc("1000"), maxQualifiedReferrers: 100, startTime: 1000000, endTime: 2000000, @@ -506,8 +511,9 @@ describe("makeReferrerEditionMetricsSchema", () => { type: ReferrerEditionMetricsTypeIds.Ranked, rules: { awardModel: ReferralProgramAwardModels.RevShareLimit, - totalAwardPoolValue: parseUsdc("2000"), + awardPool: parseUsdc("2000"), minBaseRevenueContribution: parseUsdc("10"), + baseAnnualRevenueContribution: parseUsdc("5"), maxBaseRevenueShare: 0.5, startTime: 1000000, endTime: 2000000, diff --git a/packages/ens-referrals/src/v1/award-models/pie-split/api/serialize.ts b/packages/ens-referrals/src/v1/award-models/pie-split/api/serialize.ts index 0d53b2811c..505aaa3953 100644 --- a/packages/ens-referrals/src/v1/award-models/pie-split/api/serialize.ts +++ b/packages/ens-referrals/src/v1/award-models/pie-split/api/serialize.ts @@ -31,7 +31,7 @@ export function serializeReferralProgramRulesPieSplit( ): SerializedReferralProgramRulesPieSplit { return { awardModel: rules.awardModel, - totalAwardPoolValue: serializePriceUsdc(rules.totalAwardPoolValue), + awardPool: serializePriceUsdc(rules.awardPool), maxQualifiedReferrers: rules.maxQualifiedReferrers, startTime: rules.startTime, endTime: rules.endTime, diff --git a/packages/ens-referrals/src/v1/award-models/pie-split/api/serialized-types.ts b/packages/ens-referrals/src/v1/award-models/pie-split/api/serialized-types.ts index 4f354b8ae0..25aa9afd6c 100644 --- a/packages/ens-referrals/src/v1/award-models/pie-split/api/serialized-types.ts +++ b/packages/ens-referrals/src/v1/award-models/pie-split/api/serialized-types.ts @@ -15,8 +15,8 @@ import type { ReferralProgramRulesPieSplit } from "../rules"; * Serialized representation of {@link ReferralProgramRulesPieSplit}. */ export interface SerializedReferralProgramRulesPieSplit - extends Omit { - totalAwardPoolValue: SerializedPriceUsdc; + extends Omit { + awardPool: SerializedPriceUsdc; rulesUrl: string; } diff --git a/packages/ens-referrals/src/v1/award-models/pie-split/api/zod-schemas.ts b/packages/ens-referrals/src/v1/award-models/pie-split/api/zod-schemas.ts index 2bddc1be79..ab03f5376d 100644 --- a/packages/ens-referrals/src/v1/award-models/pie-split/api/zod-schemas.ts +++ b/packages/ens-referrals/src/v1/award-models/pie-split/api/zod-schemas.ts @@ -28,7 +28,7 @@ export const makeReferralProgramRulesPieSplitSchema = ( ) => makeBaseReferralProgramRulesSchema(valueLabel).safeExtend({ awardModel: z.literal(ReferralProgramAwardModels.PieSplit), - totalAwardPoolValue: makePriceUsdcSchema(`${valueLabel}.totalAwardPoolValue`), + awardPool: makePriceUsdcSchema(`${valueLabel}.awardPool`), maxQualifiedReferrers: makeNonNegativeIntegerSchema(`${valueLabel}.maxQualifiedReferrers`), }); diff --git a/packages/ens-referrals/src/v1/award-models/pie-split/metrics.ts b/packages/ens-referrals/src/v1/award-models/pie-split/metrics.ts index 6628a34ae0..b717feb61e 100644 --- a/packages/ens-referrals/src/v1/award-models/pie-split/metrics.ts +++ b/packages/ens-referrals/src/v1/award-models/pie-split/metrics.ts @@ -174,10 +174,10 @@ export interface AwardedReferrerMetricsPieSplit extends RankedReferrerMetricsPie awardPoolShare: number; /** - * The approximate USDC value of the referrer's share of the {@link ReferralProgramRulesPieSplit.totalAwardPoolValue}. + * The approximate USDC value of the referrer's share of the {@link ReferralProgramRulesPieSplit.awardPool}. * - * @invariant Guaranteed to be a valid PriceUsdc with amount between 0 and {@link ReferralProgramRulesPieSplit.totalAwardPoolValue.amount} (inclusive) - * @invariant Calculated as: `awardPoolShare` * {@link ReferralProgramRulesPieSplit.totalAwardPoolValue.amount} + * @invariant Guaranteed to be a valid PriceUsdc with amount between 0 and {@link ReferralProgramRulesPieSplit.awardPool.amount} (inclusive) + * @invariant Calculated as: `awardPoolShare` * {@link ReferralProgramRulesPieSplit.awardPool.amount} */ awardPoolApproxValue: PriceUsdc; } @@ -197,9 +197,9 @@ export const validateAwardedReferrerMetricsPieSplit = ( referrer.awardPoolApproxValue, ); - if (referrer.awardPoolApproxValue.amount > rules.totalAwardPoolValue.amount) { + if (referrer.awardPoolApproxValue.amount > rules.awardPool.amount) { throw new Error( - `AwardedReferrerMetricsPieSplit: awardPoolApproxValue.amount ${referrer.awardPoolApproxValue.amount.toString()} exceeds totalAwardPoolValue.amount ${rules.totalAwardPoolValue.amount.toString()}.`, + `AwardedReferrerMetricsPieSplit: awardPoolApproxValue.amount ${referrer.awardPoolApproxValue.amount.toString()} exceeds awardPool.amount ${rules.awardPool.amount.toString()}.`, ); } }; @@ -211,8 +211,8 @@ export const buildAwardedReferrerMetricsPieSplit = ( ): AwardedReferrerMetricsPieSplit => { const awardPoolShare = calcReferrerAwardPoolSharePieSplit(referrer, aggregatedMetrics); - // Calculate the approximate USDC value by multiplying the share by the total award pool value - const awardPoolApproxValue = scalePrice(rules.totalAwardPoolValue, awardPoolShare); + // Calculate the approximate USDC value by multiplying the share by the award pool + const awardPoolApproxValue = scalePrice(rules.awardPool, awardPoolShare); const result = { ...referrer, diff --git a/packages/ens-referrals/src/v1/award-models/pie-split/rules.ts b/packages/ens-referrals/src/v1/award-models/pie-split/rules.ts index 797099bd0d..960546e9d9 100644 --- a/packages/ens-referrals/src/v1/award-models/pie-split/rules.ts +++ b/packages/ens-referrals/src/v1/award-models/pie-split/rules.ts @@ -18,11 +18,11 @@ export interface ReferralProgramRulesPieSplit extends BaseReferralProgramRules { awardModel: typeof ReferralProgramAwardModels.PieSplit; /** - * The total value of the award pool in USDC. + * The award pool in USDC. * * NOTE: Awards will actually be distributed in $ENS tokens. */ - totalAwardPoolValue: PriceUsdc; + awardPool: PriceUsdc; /** * The maximum number of referrers that will qualify to receive a non-zero `awardPoolShare`. @@ -33,9 +33,7 @@ export interface ReferralProgramRulesPieSplit extends BaseReferralProgramRules { } export const validateReferralProgramRulesPieSplit = (rules: ReferralProgramRulesPieSplit): void => { - makePriceUsdcSchema("ReferralProgramRulesPieSplit.totalAwardPoolValue").parse( - rules.totalAwardPoolValue, - ); + makePriceUsdcSchema("ReferralProgramRulesPieSplit.awardPool").parse(rules.awardPool); validateNonNegativeInteger(rules.maxQualifiedReferrers); @@ -43,7 +41,7 @@ export const validateReferralProgramRulesPieSplit = (rules: ReferralProgramRules }; export const buildReferralProgramRulesPieSplit = ( - totalAwardPoolValue: PriceUsdc, + awardPool: PriceUsdc, maxQualifiedReferrers: number, startTime: UnixTimestamp, endTime: UnixTimestamp, @@ -53,7 +51,7 @@ export const buildReferralProgramRulesPieSplit = ( ): ReferralProgramRulesPieSplit => { const result = { awardModel: ReferralProgramAwardModels.PieSplit, - totalAwardPoolValue, + awardPool, maxQualifiedReferrers, startTime, endTime, diff --git a/packages/ens-referrals/src/v1/award-models/rev-share-limit/api/serialize.ts b/packages/ens-referrals/src/v1/award-models/rev-share-limit/api/serialize.ts index a1966c87bc..2990686281 100644 --- a/packages/ens-referrals/src/v1/award-models/rev-share-limit/api/serialize.ts +++ b/packages/ens-referrals/src/v1/award-models/rev-share-limit/api/serialize.ts @@ -34,8 +34,9 @@ export function serializeReferralProgramRulesRevShareLimit( ): SerializedReferralProgramRulesRevShareLimit { return { awardModel: rules.awardModel, - totalAwardPoolValue: serializePriceUsdc(rules.totalAwardPoolValue), + awardPool: serializePriceUsdc(rules.awardPool), minBaseRevenueContribution: serializePriceUsdc(rules.minBaseRevenueContribution), + baseAnnualRevenueContribution: serializePriceUsdc(rules.baseAnnualRevenueContribution), maxBaseRevenueShare: rules.maxBaseRevenueShare, startTime: rules.startTime, endTime: rules.endTime, diff --git a/packages/ens-referrals/src/v1/award-models/rev-share-limit/api/serialized-types.ts b/packages/ens-referrals/src/v1/award-models/rev-share-limit/api/serialized-types.ts index db0b42af87..b8c45ed09f 100644 --- a/packages/ens-referrals/src/v1/award-models/rev-share-limit/api/serialized-types.ts +++ b/packages/ens-referrals/src/v1/award-models/rev-share-limit/api/serialized-types.ts @@ -20,10 +20,11 @@ import type { ReferralProgramRulesRevShareLimit } from "../rules"; export interface SerializedReferralProgramRulesRevShareLimit extends Omit< ReferralProgramRulesRevShareLimit, - "totalAwardPoolValue" | "minBaseRevenueContribution" | "rulesUrl" + "awardPool" | "minBaseRevenueContribution" | "baseAnnualRevenueContribution" | "rulesUrl" > { - totalAwardPoolValue: SerializedPriceUsdc; + awardPool: SerializedPriceUsdc; minBaseRevenueContribution: SerializedPriceUsdc; + baseAnnualRevenueContribution: SerializedPriceUsdc; rulesUrl: string; } diff --git a/packages/ens-referrals/src/v1/award-models/rev-share-limit/api/zod-schemas.ts b/packages/ens-referrals/src/v1/award-models/rev-share-limit/api/zod-schemas.ts index fbda2cf37d..3359960eee 100644 --- a/packages/ens-referrals/src/v1/award-models/rev-share-limit/api/zod-schemas.ts +++ b/packages/ens-referrals/src/v1/award-models/rev-share-limit/api/zod-schemas.ts @@ -40,8 +40,11 @@ export const makeReferralProgramRulesRevShareLimitSchema = ( ) => makeBaseReferralProgramRulesSchema(valueLabel).safeExtend({ awardModel: z.literal(ReferralProgramAwardModels.RevShareLimit), - totalAwardPoolValue: makePriceUsdcSchema(`${valueLabel}.totalAwardPoolValue`), + awardPool: makePriceUsdcSchema(`${valueLabel}.awardPool`), minBaseRevenueContribution: makePriceUsdcSchema(`${valueLabel}.minBaseRevenueContribution`), + baseAnnualRevenueContribution: makePriceUsdcSchema( + `${valueLabel}.baseAnnualRevenueContribution`, + ), maxBaseRevenueShare: makeFiniteNonNegativeNumberSchema(`${valueLabel}.maxBaseRevenueShare`).max( 1, `${valueLabel}.maxBaseRevenueShare must be <= 1`, @@ -136,6 +139,22 @@ export const makeUnrankedReferrerMetricsRevShareLimitSchema = ( .min(1, `${valueLabel}.adminDisqualificationReason must not be empty`) .nullable(), }) + .refine((data) => data.totalReferrals === 0, { + message: `${valueLabel}.totalReferrals must be 0 for unranked referrers`, + path: ["totalReferrals"], + }) + .refine((data) => data.totalIncrementalDuration === 0, { + message: `${valueLabel}.totalIncrementalDuration must be 0 for unranked referrers`, + path: ["totalIncrementalDuration"], + }) + .refine((data) => data.totalRevenueContribution.amount === 0n, { + message: `${valueLabel}.totalRevenueContribution must be 0 for unranked referrers`, + path: ["totalRevenueContribution"], + }) + .refine((data) => data.totalBaseRevenueContribution.amount === 0n, { + message: `${valueLabel}.totalBaseRevenueContribution must be 0 for unranked referrers`, + path: ["totalBaseRevenueContribution"], + }) .refine((data) => data.uncappedAwardValue.amount === 0n, { message: `${valueLabel}.uncappedAwardValue must be 0 for unranked referrers`, path: ["uncappedAwardValue"], diff --git a/packages/ens-referrals/src/v1/award-models/rev-share-limit/leaderboard.test.ts b/packages/ens-referrals/src/v1/award-models/rev-share-limit/leaderboard.test.ts index 74b4d879bc..a8d790a333 100644 --- a/packages/ens-referrals/src/v1/award-models/rev-share-limit/leaderboard.test.ts +++ b/packages/ens-referrals/src/v1/award-models/rev-share-limit/leaderboard.test.ts @@ -36,22 +36,23 @@ const CHECKPOINT_PREFIX = /** * Build test rules. * - * - BASE_REVENUE_CONTRIBUTION_PER_YEAR = $5 USDC + * - baseAnnualRevenueContribution = $5 USDC * - maxBaseRevenueShare = 0.5 * - 1 year of duration → $5 base revenue → $2.50 standard award * - minBaseRevenueContribution = $5 → need exactly 1 year to qualify * - * @param totalAwardPoolValue - USDC amount for the pool (default: $1000) + * @param awardPool - USDC amount for the pool (default: $1000) * @param minBaseRevenueContribution - USDC threshold (default: $5 = 1 year) */ function buildTestRules( - totalAwardPoolValue = parseUsdc("1000"), + awardPool = parseUsdc("1000"), minBaseRevenueContribution = parseUsdc("5"), disqualifications: ReferralProgramEditionDisqualification[] = [], ) { return buildReferralProgramRulesRevShareLimit( - totalAwardPoolValue, + awardPool, minBaseRevenueContribution, + parseUsdc("5"), // baseAnnualRevenueContribution 0.5, // maxBaseRevenueShare parseTimestamp("2026-01-01T00:00:00Z"), parseTimestamp("2026-12-31T23:59:59Z"), @@ -109,7 +110,7 @@ describe("buildReferrerLeaderboardRevShareLimit", () => { grandTotalReferrals: 0, grandTotalIncrementalDuration: 0, grandTotalRevenueContribution: ZERO_ETH, - awardPoolRemaining: rules.totalAwardPoolValue, + awardPoolRemaining: rules.awardPool, }); }); @@ -129,9 +130,7 @@ describe("buildReferrerLeaderboardRevShareLimit", () => { expect(referrer.cappedAwardValue.amount).toBe(0n); // Pool should be fully intact - expect(result.aggregatedMetrics.awardPoolRemaining.amount).toBe( - rules.totalAwardPoolValue.amount, - ); + expect(result.aggregatedMetrics.awardPoolRemaining.amount).toBe(rules.awardPool.amount); }); }); diff --git a/packages/ens-referrals/src/v1/award-models/rev-share-limit/leaderboard.ts b/packages/ens-referrals/src/v1/award-models/rev-share-limit/leaderboard.ts index bea04a39da..e8902f1c4e 100644 --- a/packages/ens-referrals/src/v1/award-models/rev-share-limit/leaderboard.ts +++ b/packages/ens-referrals/src/v1/award-models/rev-share-limit/leaderboard.ts @@ -21,13 +21,11 @@ import { buildReferrerMetricsRevShareLimit, } from "./metrics"; import type { ReferralEvent } from "./referral-event"; -import { - BASE_REVENUE_CONTRIBUTION_PER_YEAR, - isReferrerQualifiedRevShareLimit, - type ReferralProgramRulesRevShareLimit, -} from "./rules"; +import { isReferrerQualifiedRevShareLimit, type ReferralProgramRulesRevShareLimit } from "./rules"; import { sortReferralEvents } from "./sort-referral-events"; +const bigintMin = (a: bigint, b: bigint): bigint => (a < b ? a : b); + /** * Represents a leaderboard with the rev-share-limit award model for any number of referrers. */ @@ -79,8 +77,8 @@ interface ReferrerRaceState { totalRevenueContributionAmount: bigint; /** Whether this referrer has ever crossed the qualification threshold. */ wasQualified: boolean; - /** Amount actually claimed from the award pool. */ - qualifiedAwardValueAmount: bigint; + /** Amount actually claimed from the award pool (the capped award). */ + cappedAwardAmount: bigint; } /** @@ -106,7 +104,7 @@ export const buildReferrerLeaderboardRevShareLimit = ( // 2. Process events sequentially to run the race. const referrerStates = new Map(); - let poolRemainingAmount = rules.totalAwardPoolValue.amount; + let poolRemainingAmount = rules.awardPool.amount; for (const event of sortedEvents) { const referrer = normalizeAddress(event.referrer); @@ -118,7 +116,7 @@ export const buildReferrerLeaderboardRevShareLimit = ( totalIncrementalDuration: 0, totalRevenueContributionAmount: 0n, wasQualified: false, - qualifiedAwardValueAmount: 0n, + cappedAwardAmount: 0n, }; referrerStates.set(referrer, state); } @@ -131,7 +129,7 @@ export const buildReferrerLeaderboardRevShareLimit = ( // Compute totalBaseRevenue from aggregated duration (single division — avoids per-event // truncation that would compound into a sum lower than the correct aggregated value). const totalBaseRevenueAmount = - (BASE_REVENUE_CONTRIBUTION_PER_YEAR.amount * BigInt(state.totalIncrementalDuration)) / + (rules.baseAnnualRevenueContribution.amount * BigInt(state.totalIncrementalDuration)) / BigInt(SECONDS_PER_YEAR); // Determine if newly qualifying or already qualified. @@ -142,40 +140,34 @@ export const buildReferrerLeaderboardRevShareLimit = ( ); if (isNowQualified && !state.wasQualified) { - // First time crossing the qualification threshold: claim all accumulated standard award. + // First time crossing the qualification threshold: claim all accumulated uncapped award. // Compute from aggregated totals to match the single-division used in final output. - const accumulatedStandardAwardAmount = scalePrice( + const accumulatedUncappedAward = scalePrice( priceUsdc(totalBaseRevenueAmount), rules.maxBaseRevenueShare, ).amount; - const claimAmount = - accumulatedStandardAwardAmount < poolRemainingAmount - ? accumulatedStandardAwardAmount - : poolRemainingAmount; - state.qualifiedAwardValueAmount += claimAmount; - poolRemainingAmount -= claimAmount; + const incrementalCappedAward = bigintMin(accumulatedUncappedAward, poolRemainingAmount); + state.cappedAwardAmount += incrementalCappedAward; + poolRemainingAmount -= incrementalCappedAward; state.wasQualified = true; } else if (state.wasQualified) { - // Already qualified: claim this event's incremental standard award. + // Already qualified: claim this event's incremental uncapped award. const incrementalBaseRevenueAmount = - (BASE_REVENUE_CONTRIBUTION_PER_YEAR.amount * BigInt(event.incrementalDuration)) / + (rules.baseAnnualRevenueContribution.amount * BigInt(event.incrementalDuration)) / BigInt(SECONDS_PER_YEAR); - const incrementalStandardAwardAmount = scalePrice( + const incrementalUncappedAward = scalePrice( priceUsdc(incrementalBaseRevenueAmount), rules.maxBaseRevenueShare, ).amount; - const claimAmount = - incrementalStandardAwardAmount < poolRemainingAmount - ? incrementalStandardAwardAmount - : poolRemainingAmount; - state.qualifiedAwardValueAmount += claimAmount; - poolRemainingAmount -= claimAmount; + const incrementalCappedAward = bigintMin(incrementalUncappedAward, poolRemainingAmount); + state.cappedAwardAmount += incrementalCappedAward; + poolRemainingAmount -= incrementalCappedAward; } // If not yet qualified, nothing is claimed from the pool. } // 3. Sort referrers to assign ranks: - // 1. qualifiedAwardValue (cappedAwardValue) desc — actual pool claims, race winners first + // 1. cappedAwardValue desc — actual pool claims, race winners first // 2. totalIncrementalDuration desc — tie-break for pool-depleted referrers // 3. referrer address desc — deterministic tie-break // Both `a` and `b` are keys from `referrerStates`, so lookups are always defined. @@ -183,9 +175,9 @@ export const buildReferrerLeaderboardRevShareLimit = ( const stateA = referrerStates.get(a) as ReferrerRaceState; const stateB = referrerStates.get(b) as ReferrerRaceState; - // Primary: qualifiedAwardValue desc (bigint comparison) - if (stateB.qualifiedAwardValueAmount !== stateA.qualifiedAwardValueAmount) { - return stateB.qualifiedAwardValueAmount > stateA.qualifiedAwardValueAmount ? 1 : -1; + // Primary: cappedAwardValue desc (bigint comparison) + if (stateB.cappedAwardAmount !== stateA.cappedAwardAmount) { + return stateB.cappedAwardAmount > stateA.cappedAwardAmount ? 1 : -1; } // Secondary: totalIncrementalDuration desc (used directly as the tie-breaker). @@ -213,7 +205,7 @@ export const buildReferrerLeaderboardRevShareLimit = ( priceEth(state.totalRevenueContributionAmount), ); - const revShareMetrics = buildReferrerMetricsRevShareLimit(baseMetrics); + const revShareMetrics = buildReferrerMetricsRevShareLimit(baseMetrics, rules); const rankedMetrics = buildRankedReferrerMetricsRevShareLimit( revShareMetrics, @@ -229,7 +221,7 @@ export const buildReferrerLeaderboardRevShareLimit = ( return buildAwardedReferrerMetricsRevShareLimit( rankedMetrics, uncappedAwardValue, - priceUsdc(state.qualifiedAwardValueAmount), + priceUsdc(state.cappedAwardAmount), rules, ); }, diff --git a/packages/ens-referrals/src/v1/award-models/rev-share-limit/metrics.ts b/packages/ens-referrals/src/v1/award-models/rev-share-limit/metrics.ts index af120e8857..11d0521173 100644 --- a/packages/ens-referrals/src/v1/award-models/rev-share-limit/metrics.ts +++ b/packages/ens-referrals/src/v1/award-models/rev-share-limit/metrics.ts @@ -8,32 +8,30 @@ import { buildReferrerMetrics, validateReferrerMetrics } from "../../referrer-me import { SECONDS_PER_YEAR } from "../../time"; import type { ReferrerRank } from "../shared/rank"; import { validateReferrerRank } from "../shared/rank"; -import { - BASE_REVENUE_CONTRIBUTION_PER_YEAR, - isReferrerQualifiedRevShareLimit, - type ReferralProgramRulesRevShareLimit, -} from "./rules"; +import { isReferrerQualifiedRevShareLimit, type ReferralProgramRulesRevShareLimit } from "./rules"; /** * Extends {@link ReferrerMetrics} with computed base revenue contribution. */ export interface ReferrerMetricsRevShareLimit extends ReferrerMetrics { /** - * The referrer's base revenue contribution (base-fee-only: $5 × years of incremental duration). + * The referrer's base revenue contribution + * (base-fee-only: `rules.baseAnnualRevenueContribution` × years of incremental duration). * Used for qualification and award calculation in the rev-share-limit model. * - * @invariant Guaranteed to be `priceUsdc(BASE_REVENUE_CONTRIBUTION_PER_YEAR.amount * BigInt(totalIncrementalDuration) / BigInt(SECONDS_PER_YEAR))` + * @invariant Guaranteed to be `priceUsdc(rules.baseAnnualRevenueContribution.amount * BigInt(totalIncrementalDuration) / BigInt(SECONDS_PER_YEAR))` */ totalBaseRevenueContribution: PriceUsdc; } export const validateReferrerMetricsRevShareLimit = ( metrics: ReferrerMetricsRevShareLimit, + rules: ReferralProgramRulesRevShareLimit, ): void => { validateReferrerMetrics(metrics); const expectedTotalBaseRevenueContribution = priceUsdc( - (BASE_REVENUE_CONTRIBUTION_PER_YEAR.amount * BigInt(metrics.totalIncrementalDuration)) / + (rules.baseAnnualRevenueContribution.amount * BigInt(metrics.totalIncrementalDuration)) / BigInt(SECONDS_PER_YEAR), ); if (metrics.totalBaseRevenueContribution.amount !== expectedTotalBaseRevenueContribution.amount) { @@ -45,9 +43,10 @@ export const validateReferrerMetricsRevShareLimit = ( export const buildReferrerMetricsRevShareLimit = ( metrics: ReferrerMetrics, + rules: ReferralProgramRulesRevShareLimit, ): ReferrerMetricsRevShareLimit => { const totalBaseRevenueContribution = priceUsdc( - (BASE_REVENUE_CONTRIBUTION_PER_YEAR.amount * BigInt(metrics.totalIncrementalDuration)) / + (rules.baseAnnualRevenueContribution.amount * BigInt(metrics.totalIncrementalDuration)) / BigInt(SECONDS_PER_YEAR), ); @@ -56,7 +55,7 @@ export const buildReferrerMetricsRevShareLimit = ( totalBaseRevenueContribution, } satisfies ReferrerMetricsRevShareLimit; - validateReferrerMetricsRevShareLimit(result); + validateReferrerMetricsRevShareLimit(result, rules); return result; }; @@ -98,7 +97,7 @@ export const validateRankedReferrerMetricsRevShareLimit = ( metrics: RankedReferrerMetricsRevShareLimit, rules: ReferralProgramRulesRevShareLimit, ): void => { - validateReferrerMetricsRevShareLimit(metrics); + validateReferrerMetricsRevShareLimit(metrics, rules); validateReferrerRank(metrics.rank); const expectedIsQualified = isReferrerQualifiedRevShareLimit( @@ -158,21 +157,21 @@ export const buildRankedReferrerMetricsRevShareLimit = ( */ export interface AwardedReferrerMetricsRevShareLimit extends RankedReferrerMetricsRevShareLimit { /** - * The standard (uncapped) USDC award value for this referrer, computed as + * The uncapped USDC award value for this referrer, computed as * `maxBaseRevenueShare × totalBaseRevenueContribution`. * * Represents what the referrer would receive if the pool were unlimited and the referrer were qualified. - * Independent of the pool state and qualification status. + * Independent of the pool state, qualification status, and admin disqualification status. */ uncappedAwardValue: PriceUsdc; /** - * The approximate USDC value of the referrer's award. + * The USDC value of the referrer's (tentative) award. * * This is the amount actually claimed from the pool by this referrer, capped by - * the remaining pool at the time of their qualifying events. + * the remaining pool at the time of their qualifying referrals. * - * @invariant Guaranteed to be a valid PriceUsdc with amount between 0 and {@link ReferralProgramRulesRevShareLimit.totalAwardPoolValue.amount} (inclusive) + * @invariant Guaranteed to be a valid PriceUsdc with amount between 0 and {@link ReferralProgramRulesRevShareLimit.awardPool.amount} (inclusive) * @invariant Always <= uncappedAwardValue.amount * @invariant Amount equal to 0 when {@link isAdminDisqualified} is true. * @invariant Amount equal to 0 when {@link isQualified} is false. @@ -206,9 +205,9 @@ export const validateAwardedReferrerMetricsRevShareLimit = ( ); } - if (metrics.cappedAwardValue.amount > rules.totalAwardPoolValue.amount) { + if (metrics.cappedAwardValue.amount > rules.awardPool.amount) { throw new Error( - `AwardedReferrerMetricsRevShareLimit: cappedAwardValue.amount ${metrics.cappedAwardValue.amount.toString()} exceeds totalAwardPoolValue.amount ${rules.totalAwardPoolValue.amount.toString()}.`, + `AwardedReferrerMetricsRevShareLimit: cappedAwardValue.amount ${metrics.cappedAwardValue.amount.toString()} exceeds awardPool.amount ${rules.awardPool.amount.toString()}.`, ); } diff --git a/packages/ens-referrals/src/v1/award-models/rev-share-limit/rules.ts b/packages/ens-referrals/src/v1/award-models/rev-share-limit/rules.ts index 6f705401d5..f5d5a2813b 100644 --- a/packages/ens-referrals/src/v1/award-models/rev-share-limit/rules.ts +++ b/packages/ens-referrals/src/v1/award-models/rev-share-limit/rules.ts @@ -1,11 +1,6 @@ import type { Address } from "viem"; -import { - type AccountId, - type PriceUsdc, - parseUsdc, - type UnixTimestamp, -} from "@ensnode/ensnode-sdk"; +import type { AccountId, PriceUsdc, UnixTimestamp } from "@ensnode/ensnode-sdk"; import { makePriceUsdcSchema } from "@ensnode/ensnode-sdk/internal"; import { normalizeAddress, validateLowercaseAddress } from "../../address"; @@ -34,36 +29,37 @@ export interface ReferralProgramEditionDisqualification { reason: string; } -/** - * Base revenue contribution per year of incremental duration. - * - * Used in `rev-share-limit` qualification and award calculations: - * 1 year of incremental duration = $5 in base revenue (base-fee-only, excluding premiums). - */ -export const BASE_REVENUE_CONTRIBUTION_PER_YEAR: PriceUsdc = parseUsdc("5"); - export interface ReferralProgramRulesRevShareLimit extends BaseReferralProgramRules { /** * Discriminant: identifies this as a "rev-share-limit" award model edition. * * In rev-share-limit, each qualified referrer receives a share of their base revenue - * contribution (base-fee-only: $5 × years of incremental duration), subject to a - * pool cap and a minimum qualification threshold. + * contribution (base-fee-only: `baseAnnualRevenueContribution` × years of incremental duration), + * subject to a pool cap and a minimum qualification threshold. */ awardModel: typeof ReferralProgramAwardModels.RevShareLimit; /** - * The total value of the award pool in USDC (acts as a cap on total payouts). + * The award pool in USDC (acts as a cap on total payouts). */ - totalAwardPoolValue: PriceUsdc; + awardPool: PriceUsdc; /** - * The minimum base revenue contribution required for a referrer to qualify. + * The minimum base revenue contribution required for a referrer to qualify for awards. */ minBaseRevenueContribution: PriceUsdc; /** - * The fraction of the referrer's base revenue contribution that constitutes their potential award. + * Base revenue contribution per year of incremental duration in USDC. + * + * Used in `rev-share-limit` qualification and award calculations: + * 1 year of incremental duration → this many USDC of base revenue (base-fee-only, excluding premiums). + */ + baseAnnualRevenueContribution: PriceUsdc; + + /** + * The fraction of the referrer's base revenue contribution that constitutes their max potential award. + * This is the max that ignores the possibility of the award pool becoming exhausted. * * @invariant Guaranteed to be a number between 0 and 1 (inclusive) */ @@ -81,14 +77,16 @@ export interface ReferralProgramRulesRevShareLimit extends BaseReferralProgramRu export const validateReferralProgramRulesRevShareLimit = ( rules: ReferralProgramRulesRevShareLimit, ): void => { - makePriceUsdcSchema("ReferralProgramRulesRevShareLimit.totalAwardPoolValue").parse( - rules.totalAwardPoolValue, - ); + makePriceUsdcSchema("ReferralProgramRulesRevShareLimit.awardPool").parse(rules.awardPool); makePriceUsdcSchema("ReferralProgramRulesRevShareLimit.minBaseRevenueContribution").parse( rules.minBaseRevenueContribution, ); + makePriceUsdcSchema("ReferralProgramRulesRevShareLimit.baseAnnualRevenueContribution").parse( + rules.baseAnnualRevenueContribution, + ); + if ( !Number.isFinite(rules.maxBaseRevenueShare) || rules.maxBaseRevenueShare < 0 || @@ -120,8 +118,9 @@ export const validateReferralProgramRulesRevShareLimit = ( }; export const buildReferralProgramRulesRevShareLimit = ( - totalAwardPoolValue: PriceUsdc, + awardPool: PriceUsdc, minBaseRevenueContribution: PriceUsdc, + baseAnnualRevenueContribution: PriceUsdc, maxBaseRevenueShare: number, startTime: UnixTimestamp, endTime: UnixTimestamp, @@ -132,8 +131,9 @@ export const buildReferralProgramRulesRevShareLimit = ( ): ReferralProgramRulesRevShareLimit => { const result = { awardModel: ReferralProgramAwardModels.RevShareLimit, - totalAwardPoolValue, + awardPool, minBaseRevenueContribution, + baseAnnualRevenueContribution, maxBaseRevenueShare, startTime, endTime, diff --git a/packages/ens-referrals/src/v1/edition-defaults.ts b/packages/ens-referrals/src/v1/edition-defaults.ts index 24b89bcc37..1bc6b5be29 100644 --- a/packages/ens-referrals/src/v1/edition-defaults.ts +++ b/packages/ens-referrals/src/v1/edition-defaults.ts @@ -48,6 +48,7 @@ export function getDefaultReferralProgramEditionConfigSet( rules: buildReferralProgramRulesRevShareLimit( parseUsdc("10000"), parseUsdc("500"), + parseUsdc("5"), 0.5, parseTimestamp("2026-03-01T00:00:00Z"), parseTimestamp("2026-03-31T23:59:59Z"), diff --git a/packages/ens-referrals/src/v1/leaderboard-page.test.ts b/packages/ens-referrals/src/v1/leaderboard-page.test.ts index dbb3a7d61d..4e9c5c5428 100644 --- a/packages/ens-referrals/src/v1/leaderboard-page.test.ts +++ b/packages/ens-referrals/src/v1/leaderboard-page.test.ts @@ -23,7 +23,7 @@ describe("buildReferrerLeaderboardPageContext", () => { awardModel: ReferralProgramAwardModels.PieSplit, rules: { awardModel: ReferralProgramAwardModels.PieSplit, - totalAwardPoolValue: priceUsdc(10000n), + awardPool: priceUsdc(10000n), maxQualifiedReferrers: 10, startTime: 1764547200, endTime: 1767225599, @@ -112,7 +112,7 @@ describe("buildReferrerLeaderboardPageContext", () => { awardModel: ReferralProgramAwardModels.PieSplit, rules: { awardModel: ReferralProgramAwardModels.PieSplit, - totalAwardPoolValue: priceUsdc(10000n), + awardPool: priceUsdc(10000n), maxQualifiedReferrers: 10, startTime: 1764547200, endTime: 1767225599, From 85336c2947365ebb89bd7545d7950560ecb6de40 Mon Sep 17 00:00:00 2001 From: Goader Date: Wed, 8 Apr 2026 12:47:51 +0200 Subject: [PATCH 06/11] review applied, docs updated --- .../rev-share-limit/edition-metrics.ts | 2 +- .../rev-share-limit/leaderboard.test.ts | 76 +++++++++---------- .../rev-share-limit/leaderboard.ts | 4 +- 3 files changed, 41 insertions(+), 41 deletions(-) diff --git a/packages/ens-referrals/src/v1/award-models/rev-share-limit/edition-metrics.ts b/packages/ens-referrals/src/v1/award-models/rev-share-limit/edition-metrics.ts index 0e3d74af7d..c0cb98d752 100644 --- a/packages/ens-referrals/src/v1/award-models/rev-share-limit/edition-metrics.ts +++ b/packages/ens-referrals/src/v1/award-models/rev-share-limit/edition-metrics.ts @@ -39,7 +39,7 @@ export interface ReferrerEditionMetricsRankedRevShareLimit { * The awarded referrer metrics from the leaderboard. * * Contains all calculated metrics including rank, qualification status, - * standard award value, and award pool approximate value. + * uncapped award value, and capped award value. */ referrer: AwardedReferrerMetricsRevShareLimit; diff --git a/packages/ens-referrals/src/v1/award-models/rev-share-limit/leaderboard.test.ts b/packages/ens-referrals/src/v1/award-models/rev-share-limit/leaderboard.test.ts index a8d790a333..0fa097ff30 100644 --- a/packages/ens-referrals/src/v1/award-models/rev-share-limit/leaderboard.test.ts +++ b/packages/ens-referrals/src/v1/award-models/rev-share-limit/leaderboard.test.ts @@ -38,7 +38,7 @@ const CHECKPOINT_PREFIX = * * - baseAnnualRevenueContribution = $5 USDC * - maxBaseRevenueShare = 0.5 - * - 1 year of duration → $5 base revenue → $2.50 standard award + * - 1 year of duration → $5 base revenue → $2.50 uncapped award * - minBaseRevenueContribution = $5 → need exactly 1 year to qualify * * @param awardPool - USDC amount for the pool (default: $1000) @@ -88,8 +88,8 @@ const accurateAsOf = parseTimestamp("2026-06-01T00:00:00Z"); // ─── Helpers ───────────────────────────────────────────────────────────────── -/** $2.50 USDC in raw amount (standard award for 1 year of duration at 50% share) */ -const STANDARD_AWARD_1Y = parseUsdc("2.5"); +/** $2.50 USDC in raw amount (uncapped award for 1 year of duration at 50% share) */ +const UNCAPPED_AWARD_1Y = parseUsdc("2.5"); // ─── Tests ──────────────────────────────────────────────────────────────────── @@ -115,7 +115,7 @@ describe("buildReferrerLeaderboardRevShareLimit", () => { }); describe("Scenario A — unqualified referrer: no award claimed", () => { - it("accumulates standard award but cappedAwardValue is $0 when not qualified", () => { + it("accumulates uncapped award but cappedAwardValue is $0 when not qualified", () => { // Half a year of duration → base revenue = $2.50 (< $5 threshold) const events = [makeEvent(ADDR_A, 1000, Math.floor(SECONDS_PER_YEAR / 2))]; const rules = buildTestRules(); @@ -134,11 +134,11 @@ describe("buildReferrerLeaderboardRevShareLimit", () => { }); }); - describe("Scenario B — referrer just qualifies, claims all accumulated standard award", () => { - it("claims all accumulated standard award when qualifying (unlimited pool)", () => { + describe("Scenario B — referrer just qualifies, claims all accumulated uncapped award", () => { + it("claims all accumulated uncapped award when qualifying (unlimited pool)", () => { // Event 1: half year → base revenue = $2.50 (not qualified) // Event 2: half year → base revenue = $5.00 (just qualified!) - // Accumulated standard award = 2 × $1.25 = $2.50 + // Accumulated uncapped award = 2 × $1.25 = $2.50 const rules = buildTestRules(parseUsdc("10000")); // large pool const events = [ makeEvent(ADDR_A, 1000, Math.floor(SECONDS_PER_YEAR / 2)), @@ -149,9 +149,9 @@ describe("buildReferrerLeaderboardRevShareLimit", () => { const referrer = result.referrers.get(ADDR_A)!; expect(referrer.isQualified).toBe(true); - expect(referrer.uncappedAwardValue.amount).toBe(STANDARD_AWARD_1Y.amount); + expect(referrer.uncappedAwardValue.amount).toBe(UNCAPPED_AWARD_1Y.amount); // Claims all accumulated: 2 × $1.25 = $2.50 - expect(referrer.cappedAwardValue.amount).toBe(STANDARD_AWARD_1Y.amount); + expect(referrer.cappedAwardValue.amount).toBe(UNCAPPED_AWARD_1Y.amount); }); }); @@ -170,7 +170,7 @@ describe("buildReferrerLeaderboardRevShareLimit", () => { expect(referrer.isQualified).toBe(true); // uncappedAwardValue = $2.50 (uncapped) - expect(referrer.uncappedAwardValue.amount).toBe(STANDARD_AWARD_1Y.amount); + expect(referrer.uncappedAwardValue.amount).toBe(UNCAPPED_AWARD_1Y.amount); // cappedAwardValue capped at $1.50 (pool limit) expect(referrer.cappedAwardValue.amount).toBe(poolAmount.amount); // Pool fully depleted @@ -178,10 +178,10 @@ describe("buildReferrerLeaderboardRevShareLimit", () => { }); }); - describe("Scenario C — already qualified, claims incremental standard award per event", () => { + describe("Scenario C — already qualified, claims incremental uncapped award per event", () => { it("qualified referrer claims incremental award on subsequent events (unlimited pool)", () => { - // Event 1: 1 year → base revenue = $5 (just qualifies), accumulated standard = $2.50, claim $2.50 - // Event 2: 1 year → already qualified, incremental standard = $2.50, claim $2.50 + // Event 1: 1 year → base revenue = $5 (just qualifies), accumulated uncapped = $2.50, claim $2.50 + // Event 2: 1 year → already qualified, incremental uncapped = $2.50, claim $2.50 // Total: $5.00 const rules = buildTestRules(parseUsdc("10000")); const events = [ @@ -234,7 +234,7 @@ describe("buildReferrerLeaderboardRevShareLimit", () => { const referrer = result.referrers.get(ADDR_A)!; expect(referrer.isQualified).toBe(true); - expect(referrer.uncappedAwardValue.amount).toBe(STANDARD_AWARD_1Y.amount); + expect(referrer.uncappedAwardValue.amount).toBe(UNCAPPED_AWARD_1Y.amount); expect(referrer.cappedAwardValue.amount).toBe(0n); }); }); @@ -255,7 +255,7 @@ describe("buildReferrerLeaderboardRevShareLimit", () => { const referrerB = result.referrers.get(ADDR_B)!; expect(referrerA.isQualified).toBe(true); - expect(referrerA.cappedAwardValue.amount).toBe(STANDARD_AWARD_1Y.amount); // $2.50 + expect(referrerA.cappedAwardValue.amount).toBe(UNCAPPED_AWARD_1Y.amount); // $2.50 expect(referrerB.isQualified).toBe(true); expect(referrerB.cappedAwardValue.amount).toBe(parseUsdc("1.5").amount); // $1.50 (only remaining) @@ -278,7 +278,7 @@ describe("buildReferrerLeaderboardRevShareLimit", () => { const referrerA = result.referrers.get(ADDR_A)!; const referrerB = result.referrers.get(ADDR_B)!; - expect(referrerA.cappedAwardValue.amount).toBe(STANDARD_AWARD_1Y.amount); // $2.50 + expect(referrerA.cappedAwardValue.amount).toBe(UNCAPPED_AWARD_1Y.amount); // $2.50 expect(referrerB.cappedAwardValue.amount).toBe(0n); // $0 — pool empty expect(result.aggregatedMetrics.awardPoolRemaining.amount).toBe(0n); }); @@ -300,11 +300,11 @@ describe("buildReferrerLeaderboardRevShareLimit", () => { const referrerB = result.referrers.get(ADDR_B)!; const referrerC = result.referrers.get(ADDR_C)!; - // Non-truncated: full standard award - expect(referrerA.cappedAwardValue.amount).toBe(STANDARD_AWARD_1Y.amount); - // Partially truncated: less than standard but > 0 + // Non-truncated: full uncapped award + expect(referrerA.cappedAwardValue.amount).toBe(UNCAPPED_AWARD_1Y.amount); + // Partially truncated: less than uncapped but > 0 expect(referrerB.cappedAwardValue.amount).toBeGreaterThan(0n); - expect(referrerB.cappedAwardValue.amount).toBeLessThan(STANDARD_AWARD_1Y.amount); + expect(referrerB.cappedAwardValue.amount).toBeLessThan(UNCAPPED_AWARD_1Y.amount); // Fully truncated: pool empty expect(referrerC.cappedAwardValue.amount).toBe(0n); expect(result.aggregatedMetrics.awardPoolRemaining.amount).toBe(0n); @@ -328,17 +328,17 @@ describe("buildReferrerLeaderboardRevShareLimit", () => { const result = buildReferrerLeaderboardRevShareLimit(events, rules, accurateAsOf); // ADDR_A has the lower (earlier) id, should claim the pool first - expect(result.referrers.get(ADDR_A)!.cappedAwardValue.amount).toBe(STANDARD_AWARD_1Y.amount); + expect(result.referrers.get(ADDR_A)!.cappedAwardValue.amount).toBe(UNCAPPED_AWARD_1Y.amount); expect(result.referrers.get(ADDR_B)!.cappedAwardValue.amount).toBe(0n); }); }); describe("Ranking", () => { - it("ranks referrers by qualifiedAwardValue desc, then uncappedAwardValue desc", () => { + it("ranks referrers by cappedAwardValue desc, then uncappedAwardValue desc", () => { // Pool = $1000 (unlimited for this test) - // ADDR_A: 1 year → qualifies at t=1000, qualifiedAward = $2.50, standardAward = $2.50 - // ADDR_B: 2 years → qualifies at t=2000, qualifiedAward = $5.00, standardAward = $5.00 - // ADDR_C: 0.5 years → never qualifies, qualifiedAward = $0, standardAward = $1.25 + // ADDR_A: 1 year → qualifies at t=1000, cappedAward = $2.50, uncappedAward = $2.50 + // ADDR_B: 2 years → qualifies at t=2000, cappedAward = $5.00, uncappedAward = $5.00 + // ADDR_C: 0.5 years → never qualifies, cappedAward = $0, uncappedAward = $1.25 const rules = buildTestRules(); const events = [ makeEvent(ADDR_A, 1000, SECONDS_PER_YEAR), @@ -348,9 +348,9 @@ describe("buildReferrerLeaderboardRevShareLimit", () => { const result = buildReferrerLeaderboardRevShareLimit(events, rules, accurateAsOf); - // ADDR_B: qualifiedAward $5.00 → rank 1 (highest pool claim) - // ADDR_A: qualifiedAward $2.50 → rank 2 - // ADDR_C: qualifiedAward $0, standardAward $1.25 → rank 3 (unqualified) + // ADDR_B: cappedAward $5.00 → rank 1 (highest pool claim) + // ADDR_A: cappedAward $2.50 → rank 2 + // ADDR_C: cappedAward $0, uncappedAward $1.25 → rank 3 (unqualified) expect(result.referrers.get(ADDR_B)!.rank).toBe(1); expect(result.referrers.get(ADDR_A)!.rank).toBe(2); expect(result.referrers.get(ADDR_C)!.rank).toBe(3); @@ -358,8 +358,8 @@ describe("buildReferrerLeaderboardRevShareLimit", () => { it("two fully-truncated referrers are ranked by uncappedAwardValue desc", () => { // Pool = $0 — nobody gets pool money - // ADDR_A: 2 years → qualifies, standardAward = $5.00, qualifiedAward = $0 - // ADDR_B: 1 year → qualifies, standardAward = $2.50, qualifiedAward = $0 + // ADDR_A: 2 years → qualifies, uncappedAward = $5.00, cappedAward = $0 + // ADDR_B: 1 year → qualifies, uncappedAward = $2.50, cappedAward = $0 const rules = buildTestRules(priceUsdc(0n)); const events = [ makeEvent(ADDR_A, 1000, SECONDS_PER_YEAR * 2), @@ -368,7 +368,7 @@ describe("buildReferrerLeaderboardRevShareLimit", () => { const result = buildReferrerLeaderboardRevShareLimit(events, rules, accurateAsOf); - // Both have $0 qualifiedAward; ADDR_A has higher standardAward → rank 1 + // Both have $0 cappedAward; ADDR_A has higher uncappedAward → rank 1 expect(result.referrers.get(ADDR_A)!.rank).toBe(1); expect(result.referrers.get(ADDR_B)!.rank).toBe(2); }); @@ -435,7 +435,7 @@ describe("buildReferrerLeaderboardRevShareLimit", () => { expect(referrerA.isQualified).toBe(true); expect(referrerA.isAdminDisqualified).toBe(false); expect(referrerA.adminDisqualificationReason).toBe(null); - expect(referrerA.cappedAwardValue.amount).toBe(STANDARD_AWARD_1Y.amount); + expect(referrerA.cappedAwardValue.amount).toBe(UNCAPPED_AWARD_1Y.amount); expect(referrerB.isQualified).toBe(true); expect(referrerB.isAdminDisqualified).toBe(false); @@ -465,7 +465,7 @@ describe("buildReferrerLeaderboardRevShareLimit", () => { // Pool was not consumed by ADDR_A, so ADDR_B gets the full award expect(referrerB.isQualified).toBe(true); expect(referrerB.isAdminDisqualified).toBe(false); - expect(referrerB.cappedAwardValue.amount).toBe(STANDARD_AWARD_1Y.amount); + expect(referrerB.cappedAwardValue.amount).toBe(UNCAPPED_AWARD_1Y.amount); }); it("disqualified referrer who never met the revenue threshold: pool unchanged", () => { @@ -487,9 +487,9 @@ describe("buildReferrerLeaderboardRevShareLimit", () => { }); it("disqualified referrer ranks between qualified (pool claim) and unqualified (below threshold)", () => { - // ADDR_A: 2 years, disqualified → standardAward $5.00, pool claim $0 - // ADDR_B: 1 year, qualified → standardAward $2.50, pool claim $2.50 - // ADDR_C: 0.5 years, below threshold → standardAward $1.25, pool claim $0 + // ADDR_A: 2 years, disqualified → uncappedAward $5.00, pool claim $0 + // ADDR_B: 1 year, qualified → uncappedAward $2.50, pool claim $2.50 + // ADDR_C: 0.5 years, below threshold → uncappedAward $1.25, pool claim $0 // // Sort by pool claim desc, then duration desc: // rank 1 → ADDR_B ($2.50 claim) @@ -512,7 +512,7 @@ describe("buildReferrerLeaderboardRevShareLimit", () => { expect(referrerB.rank).toBe(1); expect(referrerB.isQualified).toBe(true); expect(referrerB.isAdminDisqualified).toBe(false); - expect(referrerB.cappedAwardValue.amount).toBe(STANDARD_AWARD_1Y.amount); + expect(referrerB.cappedAwardValue.amount).toBe(UNCAPPED_AWARD_1Y.amount); expect(referrerA.rank).toBe(2); expect(referrerA.isAdminDisqualified).toBe(true); @@ -555,7 +555,7 @@ describe("buildReferrerLeaderboardRevShareLimit", () => { expect(referrerC.isAdminDisqualified).toBe(false); expect(referrerC.adminDisqualificationReason).toBe(null); expect(referrerC.isQualified).toBe(true); - expect(referrerC.cappedAwardValue.amount).toBe(STANDARD_AWARD_1Y.amount); + expect(referrerC.cappedAwardValue.amount).toBe(UNCAPPED_AWARD_1Y.amount); }); it("duplicate address in disqualifications: buildReferralProgramRulesRevShareLimit throws", () => { diff --git a/packages/ens-referrals/src/v1/award-models/rev-share-limit/leaderboard.ts b/packages/ens-referrals/src/v1/award-models/rev-share-limit/leaderboard.ts index e8902f1c4e..f3039558c5 100644 --- a/packages/ens-referrals/src/v1/award-models/rev-share-limit/leaderboard.ts +++ b/packages/ens-referrals/src/v1/award-models/rev-share-limit/leaderboard.ts @@ -86,8 +86,8 @@ interface ReferrerRaceState { * race algorithm over individual referral events. * * Events are processed in chronological order. When a referrer first crosses the qualification - * threshold, they claim ALL accumulated standard award value at once (capped by remaining pool). - * After qualifying, each subsequent event claims that event's incremental standard award (also + * threshold, they claim ALL accumulated uncapped award at once (capped by remaining pool). + * After qualifying, each subsequent event claims that event's incremental uncapped award (also * capped). Once the pool reaches $0, no further awards are issued to anyone. * * @param events - Raw referral events from the database (unsorted; will be sorted internally). From 59ac11e75204bff8a75f9d7a14fcfc99d3c2f37b Mon Sep 17 00:00:00 2001 From: Goader Date: Wed, 8 Apr 2026 13:34:21 +0200 Subject: [PATCH 07/11] review applied, test added --- .../rev-share-limit/leaderboard.test.ts | 49 +++++++++++++++++-- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/packages/ens-referrals/src/v1/award-models/rev-share-limit/leaderboard.test.ts b/packages/ens-referrals/src/v1/award-models/rev-share-limit/leaderboard.test.ts index 0fa097ff30..ceab9eaf03 100644 --- a/packages/ens-referrals/src/v1/award-models/rev-share-limit/leaderboard.test.ts +++ b/packages/ens-referrals/src/v1/award-models/rev-share-limit/leaderboard.test.ts @@ -36,23 +36,26 @@ const CHECKPOINT_PREFIX = /** * Build test rules. * - * - baseAnnualRevenueContribution = $5 USDC + * - baseAnnualRevenueContribution = $5 USDC (default) * - maxBaseRevenueShare = 0.5 * - 1 year of duration → $5 base revenue → $2.50 uncapped award * - minBaseRevenueContribution = $5 → need exactly 1 year to qualify * * @param awardPool - USDC amount for the pool (default: $1000) * @param minBaseRevenueContribution - USDC threshold (default: $5 = 1 year) + * @param disqualifications - Admin disqualification list (default: none) + * @param baseAnnualRevenueContribution - Base revenue per year (default: $5) */ function buildTestRules( awardPool = parseUsdc("1000"), minBaseRevenueContribution = parseUsdc("5"), disqualifications: ReferralProgramEditionDisqualification[] = [], + baseAnnualRevenueContribution = parseUsdc("5"), ) { return buildReferralProgramRulesRevShareLimit( awardPool, minBaseRevenueContribution, - parseUsdc("5"), // baseAnnualRevenueContribution + baseAnnualRevenueContribution, 0.5, // maxBaseRevenueShare parseTimestamp("2026-01-01T00:00:00Z"), parseTimestamp("2026-12-31T23:59:59Z"), @@ -357,7 +360,7 @@ describe("buildReferrerLeaderboardRevShareLimit", () => { }); it("two fully-truncated referrers are ranked by uncappedAwardValue desc", () => { - // Pool = $0 — nobody gets pool money + // Pool = $0 — implementation sorts by totalIncrementalDuration desc, equivalent to uncappedAwardValue desc here. // ADDR_A: 2 years → qualifies, uncappedAward = $5.00, cappedAward = $0 // ADDR_B: 1 year → qualifies, uncappedAward = $2.50, cappedAward = $0 const rules = buildTestRules(priceUsdc(0n)); @@ -368,7 +371,7 @@ describe("buildReferrerLeaderboardRevShareLimit", () => { const result = buildReferrerLeaderboardRevShareLimit(events, rules, accurateAsOf); - // Both have $0 cappedAward; ADDR_A has higher uncappedAward → rank 1 + // Both have $0 cappedAward; ADDR_A has higher uncappedAward (longer duration) → rank 1 expect(result.referrers.get(ADDR_A)!.rank).toBe(1); expect(result.referrers.get(ADDR_B)!.rank).toBe(2); }); @@ -404,6 +407,44 @@ describe("buildReferrerLeaderboardRevShareLimit", () => { }); }); + describe("Configurable baseAnnualRevenueContribution", () => { + it("calculations scale with the configured baseAnnualRevenueContribution", () => { + // baseAnnualRevenueContribution = $10/yr (double the default $5/yr) + // maxBaseRevenueShare = 0.5 + // minBaseRevenueContribution = $10 → need exactly 1 year to qualify + // 1 year of duration → $10 base revenue → $5.00 uncapped award + // 0.5 years of duration → $5 base revenue (below $10 threshold) → not qualified + const rules = buildTestRules( + parseUsdc("1000"), // awardPool + parseUsdc("10"), // minBaseRevenueContribution + [], // disqualifications + parseUsdc("10"), // baseAnnualRevenueContribution + ); + const events = [ + makeEvent(ADDR_A, 1000, SECONDS_PER_YEAR), // qualifies: $10 base → $5 uncapped + makeEvent(ADDR_B, 2000, Math.floor(SECONDS_PER_YEAR / 2)), // below threshold: $5 base + ]; + + const result = buildReferrerLeaderboardRevShareLimit(events, rules, accurateAsOf); + const referrerA = result.referrers.get(ADDR_A)!; + const referrerB = result.referrers.get(ADDR_B)!; + + // ADDR_A: 1 year at $10/yr → $10 base → uncapped = 0.5 × $10 = $5.00 + expect(referrerA.isQualified).toBe(true); + expect(referrerA.uncappedAwardValue.amount).toBe(parseUsdc("5").amount); + expect(referrerA.cappedAwardValue.amount).toBe(parseUsdc("5").amount); + + // ADDR_B: 0.5 years at $10/yr → $5 base → below $10 threshold → not qualified + // uncapped = 0.5 × $5 = $2.50, but capped = $0 + expect(referrerB.isQualified).toBe(false); + expect(referrerB.uncappedAwardValue.amount).toBe(parseUsdc("2.5").amount); + expect(referrerB.cappedAwardValue.amount).toBe(0n); + + // Pool consumed only by ADDR_A's $5 claim + expect(result.aggregatedMetrics.awardPoolRemaining.amount).toBe(parseUsdc("995").amount); + }); + }); + describe("Aggregated metrics", () => { it("correctly sums grandTotalReferrals and grandTotalIncrementalDuration", () => { const rules = buildTestRules(); From 28ec732aacc827e56380fe31087df2ff75d599e6 Mon Sep 17 00:00:00 2001 From: Goader Date: Wed, 8 Apr 2026 15:39:28 +0200 Subject: [PATCH 08/11] review applied --- .../rev-share-limit/api/zod-schemas.ts | 14 +++++++++++++- .../src/v1/award-models/rev-share-limit/metrics.ts | 6 +++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/ens-referrals/src/v1/award-models/rev-share-limit/api/zod-schemas.ts b/packages/ens-referrals/src/v1/award-models/rev-share-limit/api/zod-schemas.ts index 3359960eee..4e9665f2d9 100644 --- a/packages/ens-referrals/src/v1/award-models/rev-share-limit/api/zod-schemas.ts +++ b/packages/ens-referrals/src/v1/award-models/rev-share-limit/api/zod-schemas.ts @@ -206,6 +206,10 @@ export const makeReferrerEditionMetricsRankedRevShareLimitSchema = ( .refine((data) => data.awardModel === data.rules.awardModel, { message: `${valueLabel}.awardModel must equal ${valueLabel}.rules.awardModel`, path: ["awardModel"], + }) + .refine((data) => data.referrer.cappedAwardValue.amount <= data.rules.awardPool.amount, { + message: `${valueLabel}.referrer.cappedAwardValue must be <= ${valueLabel}.rules.awardPool`, + path: ["referrer", "cappedAwardValue", "amount"], }); /** @@ -280,4 +284,12 @@ export const makeReferrerLeaderboardPageRevShareLimitSchema = ( .refine((data) => data.awardModel === data.rules.awardModel, { message: `${valueLabel}.awardModel must equal ${valueLabel}.rules.awardModel`, path: ["awardModel"], - }); + }) + .refine( + (data) => + data.referrers.every((r) => r.cappedAwardValue.amount <= data.rules.awardPool.amount), + { + message: `${valueLabel}.referrers[].cappedAwardValue must be <= ${valueLabel}.rules.awardPool`, + path: ["referrers"], + }, + ); diff --git a/packages/ens-referrals/src/v1/award-models/rev-share-limit/metrics.ts b/packages/ens-referrals/src/v1/award-models/rev-share-limit/metrics.ts index 11d0521173..3e913bddf4 100644 --- a/packages/ens-referrals/src/v1/award-models/rev-share-limit/metrics.ts +++ b/packages/ens-referrals/src/v1/award-models/rev-share-limit/metrics.ts @@ -69,7 +69,11 @@ export interface RankedReferrerMetricsRevShareLimit extends ReferrerMetricsRevSh rank: ReferrerRank; /** - * Identifies if the referrer meets the qualifications of the {@link ReferralProgramRulesRevShareLimit} to receive a non-zero `cappedAwardValue`. + * Identifies if the referrer is eligible for an award under the {@link ReferralProgramRulesRevShareLimit}. + * + * Note: this is a purely rule-based eligibility predicate and does NOT guarantee + * `cappedAwardValue.amount > 0n` — a qualified referrer may still receive $0 if the + * award pool is already depleted by earlier referrers in the race. * * @invariant true if and only if `totalBaseRevenueContribution` is greater than or equal to * {@link ReferralProgramRulesRevShareLimit.minBaseRevenueContribution} AND From eaa87bf9d2d6d2a2104592b82ab08c344ca9bf24 Mon Sep 17 00:00:00 2001 From: Goader Date: Wed, 8 Apr 2026 16:31:06 +0200 Subject: [PATCH 09/11] review applied --- .changeset/bright-foxes-dance.md | 2 +- packages/ens-referrals/README.md | 4 +- .../src/v1/api/zod-schemas.test.ts | 4 +- .../rev-share-limit/aggregations.ts | 2 +- .../rev-share-limit/api/serialize.ts | 8 +- .../rev-share-limit/api/serialized-types.ts | 18 ++-- .../rev-share-limit/api/zod-schemas.ts | 48 ++++----- .../rev-share-limit/edition-metrics.ts | 2 +- .../rev-share-limit/leaderboard.test.ts | 102 +++++++++--------- .../rev-share-limit/leaderboard.ts | 8 +- .../award-models/rev-share-limit/metrics.ts | 66 ++++++------ 11 files changed, 127 insertions(+), 137 deletions(-) diff --git a/.changeset/bright-foxes-dance.md b/.changeset/bright-foxes-dance.md index 73c5896e8c..5ff974fe77 100644 --- a/.changeset/bright-foxes-dance.md +++ b/.changeset/bright-foxes-dance.md @@ -2,4 +2,4 @@ "@namehash/ens-referrals": minor --- -Rename rev-share-limit API fields for clarity: `minQualifiedRevenueContribution` → `minBaseRevenueContribution`, `qualifiedRevenueShare` → `maxBaseRevenueShare`, `standardAwardValue` → `uncappedAwardValue`, `awardPoolApproxValue` → `cappedAwardValue`. Rename `totalAwardPoolValue` → `awardPool` for both rev-share-limit and pie-split rules. Extract the previously hardcoded `BASE_REVENUE_CONTRIBUTION_PER_YEAR` constant into a per-edition `baseAnnualRevenueContribution` rule field. +Rename rev-share-limit API fields for clarity: `minQualifiedRevenueContribution` → `minBaseRevenueContribution`, `qualifiedRevenueShare` → `maxBaseRevenueShare`, `standardAwardValue` → `uncappedAward`, `awardPoolApproxValue` → `cappedAward`. Rename `totalAwardPoolValue` → `awardPool` for both rev-share-limit and pie-split rules. Extract the previously hardcoded `BASE_REVENUE_CONTRIBUTION_PER_YEAR` constant into a per-edition `baseAnnualRevenueContribution` rule field. diff --git a/packages/ens-referrals/README.md b/packages/ens-referrals/README.md index ea2cf9860e..041a25ff4a 100644 --- a/packages/ens-referrals/README.md +++ b/packages/ens-referrals/README.md @@ -102,7 +102,7 @@ if (response.responseCode === ReferrerLeaderboardPageResponseCodes.Ok) { ); console.log(`Max Base Revenue Share: ${leaderboardPage.rules.maxBaseRevenueShare}`); console.log( - `Tentative award for the top ranked referrer: ${firstReferrer !== null ? firstReferrer.cappedAwardValue : noReferrersFallback}`, + `Tentative award for the top ranked referrer: ${firstReferrer !== null ? firstReferrer.cappedAward : noReferrersFallback}`, ); } } @@ -146,7 +146,7 @@ if (response.responseCode === ReferrerMetricsEditionsResponseCodes.Ok) { console.log( `Referrer's total base revenue contribution: ${detail.referrer.totalBaseRevenueContribution}`, ); - console.log(`Referrer's uncapped award value: ${detail.referrer.uncappedAwardValue}`); + console.log(`Referrer's uncapped award value: ${detail.referrer.uncappedAward}`); } } } diff --git a/packages/ens-referrals/src/v1/api/zod-schemas.test.ts b/packages/ens-referrals/src/v1/api/zod-schemas.test.ts index f93e0f87bc..d1649b1144 100644 --- a/packages/ens-referrals/src/v1/api/zod-schemas.test.ts +++ b/packages/ens-referrals/src/v1/api/zod-schemas.test.ts @@ -529,8 +529,8 @@ describe("makeReferrerEditionMetricsSchema", () => { totalBaseRevenueContribution: parseUsdc("150"), rank: 1, isQualified: true, - uncappedAwardValue: parseUsdc("200"), - cappedAwardValue: parseUsdc("200"), + uncappedAward: parseUsdc("200"), + cappedAward: parseUsdc("200"), isAdminDisqualified: false, adminDisqualificationReason: null, }, diff --git a/packages/ens-referrals/src/v1/award-models/rev-share-limit/aggregations.ts b/packages/ens-referrals/src/v1/award-models/rev-share-limit/aggregations.ts index 1a63e7b878..91f1425266 100644 --- a/packages/ens-referrals/src/v1/award-models/rev-share-limit/aggregations.ts +++ b/packages/ens-referrals/src/v1/award-models/rev-share-limit/aggregations.ts @@ -31,7 +31,7 @@ export interface AggregatedReferrerMetricsRevShareLimit { grandTotalRevenueContribution: PriceEth; /** - * The remaining amount in the award pool after subtracting all qualified awards + * The remaining amount in the award pool after subtracting all capped awards * claimed during the sequential race processing. * * @invariant Guaranteed to be a valid PriceUsdc with non-negative amount (>= 0n) diff --git a/packages/ens-referrals/src/v1/award-models/rev-share-limit/api/serialize.ts b/packages/ens-referrals/src/v1/award-models/rev-share-limit/api/serialize.ts index 2990686281..a08da2959f 100644 --- a/packages/ens-referrals/src/v1/award-models/rev-share-limit/api/serialize.ts +++ b/packages/ens-referrals/src/v1/award-models/rev-share-limit/api/serialize.ts @@ -75,8 +75,8 @@ export function serializeAwardedReferrerMetricsRevShareLimit( totalBaseRevenueContribution: serializePriceUsdc(metrics.totalBaseRevenueContribution), rank: metrics.rank, isQualified: metrics.isQualified, - uncappedAwardValue: serializePriceUsdc(metrics.uncappedAwardValue), - cappedAwardValue: serializePriceUsdc(metrics.cappedAwardValue), + uncappedAward: serializePriceUsdc(metrics.uncappedAward), + cappedAward: serializePriceUsdc(metrics.cappedAward), isAdminDisqualified: metrics.isAdminDisqualified, adminDisqualificationReason: metrics.adminDisqualificationReason, }; @@ -96,8 +96,8 @@ export function serializeUnrankedReferrerMetricsRevShareLimit( totalBaseRevenueContribution: serializePriceUsdc(metrics.totalBaseRevenueContribution), rank: metrics.rank, isQualified: metrics.isQualified, - uncappedAwardValue: serializePriceUsdc(metrics.uncappedAwardValue), - cappedAwardValue: serializePriceUsdc(metrics.cappedAwardValue), + uncappedAward: serializePriceUsdc(metrics.uncappedAward), + cappedAward: serializePriceUsdc(metrics.cappedAward), isAdminDisqualified: metrics.isAdminDisqualified, adminDisqualificationReason: metrics.adminDisqualificationReason, }; diff --git a/packages/ens-referrals/src/v1/award-models/rev-share-limit/api/serialized-types.ts b/packages/ens-referrals/src/v1/award-models/rev-share-limit/api/serialized-types.ts index b8c45ed09f..37161ed616 100644 --- a/packages/ens-referrals/src/v1/award-models/rev-share-limit/api/serialized-types.ts +++ b/packages/ens-referrals/src/v1/award-models/rev-share-limit/api/serialized-types.ts @@ -46,15 +46,12 @@ export interface SerializedAggregatedReferrerMetricsRevShareLimit export interface SerializedAwardedReferrerMetricsRevShareLimit extends Omit< AwardedReferrerMetricsRevShareLimit, - | "totalRevenueContribution" - | "totalBaseRevenueContribution" - | "uncappedAwardValue" - | "cappedAwardValue" + "totalRevenueContribution" | "totalBaseRevenueContribution" | "uncappedAward" | "cappedAward" > { totalRevenueContribution: SerializedPriceEth; totalBaseRevenueContribution: SerializedPriceUsdc; - uncappedAwardValue: SerializedPriceUsdc; - cappedAwardValue: SerializedPriceUsdc; + uncappedAward: SerializedPriceUsdc; + cappedAward: SerializedPriceUsdc; } /** @@ -63,15 +60,12 @@ export interface SerializedAwardedReferrerMetricsRevShareLimit export interface SerializedUnrankedReferrerMetricsRevShareLimit extends Omit< UnrankedReferrerMetricsRevShareLimit, - | "totalRevenueContribution" - | "totalBaseRevenueContribution" - | "uncappedAwardValue" - | "cappedAwardValue" + "totalRevenueContribution" | "totalBaseRevenueContribution" | "uncappedAward" | "cappedAward" > { totalRevenueContribution: SerializedPriceEth; totalBaseRevenueContribution: SerializedPriceUsdc; - uncappedAwardValue: SerializedPriceUsdc; - cappedAwardValue: SerializedPriceUsdc; + uncappedAward: SerializedPriceUsdc; + cappedAward: SerializedPriceUsdc; } /** diff --git a/packages/ens-referrals/src/v1/award-models/rev-share-limit/api/zod-schemas.ts b/packages/ens-referrals/src/v1/award-models/rev-share-limit/api/zod-schemas.ts index 4e9665f2d9..f6ddf361a1 100644 --- a/packages/ens-referrals/src/v1/award-models/rev-share-limit/api/zod-schemas.ts +++ b/packages/ens-referrals/src/v1/award-models/rev-share-limit/api/zod-schemas.ts @@ -82,8 +82,8 @@ export const makeAwardedReferrerMetricsRevShareLimitSchema = ( ), rank: makePositiveIntegerSchema(`${valueLabel}.rank`), isQualified: z.boolean(), - uncappedAwardValue: makePriceUsdcSchema(`${valueLabel}.uncappedAwardValue`), - cappedAwardValue: makePriceUsdcSchema(`${valueLabel}.cappedAwardValue`), + uncappedAward: makePriceUsdcSchema(`${valueLabel}.uncappedAward`), + cappedAward: makePriceUsdcSchema(`${valueLabel}.cappedAward`), isAdminDisqualified: z.boolean(), adminDisqualificationReason: z .string() @@ -91,16 +91,15 @@ export const makeAwardedReferrerMetricsRevShareLimitSchema = ( .min(1, `${valueLabel}.adminDisqualificationReason must not be empty`) .nullable(), }) - .refine((data) => data.cappedAwardValue.amount <= data.uncappedAwardValue.amount, { - message: `${valueLabel}.cappedAwardValue must be <= ${valueLabel}.uncappedAwardValue`, - path: ["cappedAwardValue"], + .refine((data) => data.cappedAward.amount <= data.uncappedAward.amount, { + message: `${valueLabel}.cappedAward must be <= ${valueLabel}.uncappedAward`, + path: ["cappedAward"], }) .refine( (data) => - !data.isAdminDisqualified || - (data.isQualified === false && data.cappedAwardValue.amount === 0n), + !data.isAdminDisqualified || (data.isQualified === false && data.cappedAward.amount === 0n), { - message: `When ${valueLabel}.isAdminDisqualified is true, isQualified must be false and cappedAwardValue.amount must be 0`, + message: `When ${valueLabel}.isAdminDisqualified is true, isQualified must be false and cappedAward.amount must be 0`, path: ["isAdminDisqualified"], }, ) @@ -108,9 +107,9 @@ export const makeAwardedReferrerMetricsRevShareLimitSchema = ( message: `${valueLabel}.adminDisqualificationReason must be non-null iff isAdminDisqualified is true`, path: ["adminDisqualificationReason"], }) - .refine((data) => data.isQualified || data.cappedAwardValue.amount === 0n, { - message: `${valueLabel}.cappedAwardValue must be 0 when isQualified is false`, - path: ["cappedAwardValue"], + .refine((data) => data.isQualified || data.cappedAward.amount === 0n, { + message: `${valueLabel}.cappedAward must be 0 when isQualified is false`, + path: ["cappedAward"], }); /** @@ -130,8 +129,8 @@ export const makeUnrankedReferrerMetricsRevShareLimitSchema = ( ), rank: z.null(), isQualified: z.literal(false), - uncappedAwardValue: makePriceUsdcSchema(`${valueLabel}.uncappedAwardValue`), - cappedAwardValue: makePriceUsdcSchema(`${valueLabel}.cappedAwardValue`), + uncappedAward: makePriceUsdcSchema(`${valueLabel}.uncappedAward`), + cappedAward: makePriceUsdcSchema(`${valueLabel}.cappedAward`), isAdminDisqualified: z.boolean(), adminDisqualificationReason: z .string() @@ -155,13 +154,13 @@ export const makeUnrankedReferrerMetricsRevShareLimitSchema = ( message: `${valueLabel}.totalBaseRevenueContribution must be 0 for unranked referrers`, path: ["totalBaseRevenueContribution"], }) - .refine((data) => data.uncappedAwardValue.amount === 0n, { - message: `${valueLabel}.uncappedAwardValue must be 0 for unranked referrers`, - path: ["uncappedAwardValue"], + .refine((data) => data.uncappedAward.amount === 0n, { + message: `${valueLabel}.uncappedAward must be 0 for unranked referrers`, + path: ["uncappedAward"], }) - .refine((data) => data.cappedAwardValue.amount === 0n, { - message: `${valueLabel}.cappedAwardValue must be 0 for unranked referrers`, - path: ["cappedAwardValue"], + .refine((data) => data.cappedAward.amount === 0n, { + message: `${valueLabel}.cappedAward must be 0 for unranked referrers`, + path: ["cappedAward"], }) .refine((data) => data.isAdminDisqualified === (data.adminDisqualificationReason !== null), { message: `${valueLabel}.adminDisqualificationReason must be non-null iff isAdminDisqualified is true`, @@ -207,9 +206,9 @@ export const makeReferrerEditionMetricsRankedRevShareLimitSchema = ( message: `${valueLabel}.awardModel must equal ${valueLabel}.rules.awardModel`, path: ["awardModel"], }) - .refine((data) => data.referrer.cappedAwardValue.amount <= data.rules.awardPool.amount, { - message: `${valueLabel}.referrer.cappedAwardValue must be <= ${valueLabel}.rules.awardPool`, - path: ["referrer", "cappedAwardValue", "amount"], + .refine((data) => data.referrer.cappedAward.amount <= data.rules.awardPool.amount, { + message: `${valueLabel}.referrer.cappedAward must be <= ${valueLabel}.rules.awardPool`, + path: ["referrer", "cappedAward", "amount"], }); /** @@ -286,10 +285,9 @@ export const makeReferrerLeaderboardPageRevShareLimitSchema = ( path: ["awardModel"], }) .refine( - (data) => - data.referrers.every((r) => r.cappedAwardValue.amount <= data.rules.awardPool.amount), + (data) => data.referrers.every((r) => r.cappedAward.amount <= data.rules.awardPool.amount), { - message: `${valueLabel}.referrers[].cappedAwardValue must be <= ${valueLabel}.rules.awardPool`, + message: `${valueLabel}.referrers[].cappedAward must be <= ${valueLabel}.rules.awardPool`, path: ["referrers"], }, ); diff --git a/packages/ens-referrals/src/v1/award-models/rev-share-limit/edition-metrics.ts b/packages/ens-referrals/src/v1/award-models/rev-share-limit/edition-metrics.ts index c0cb98d752..63317632d9 100644 --- a/packages/ens-referrals/src/v1/award-models/rev-share-limit/edition-metrics.ts +++ b/packages/ens-referrals/src/v1/award-models/rev-share-limit/edition-metrics.ts @@ -39,7 +39,7 @@ export interface ReferrerEditionMetricsRankedRevShareLimit { * The awarded referrer metrics from the leaderboard. * * Contains all calculated metrics including rank, qualification status, - * uncapped award value, and capped award value. + * uncapped award, and capped award. */ referrer: AwardedReferrerMetricsRevShareLimit; diff --git a/packages/ens-referrals/src/v1/award-models/rev-share-limit/leaderboard.test.ts b/packages/ens-referrals/src/v1/award-models/rev-share-limit/leaderboard.test.ts index ceab9eaf03..9efd10b773 100644 --- a/packages/ens-referrals/src/v1/award-models/rev-share-limit/leaderboard.test.ts +++ b/packages/ens-referrals/src/v1/award-models/rev-share-limit/leaderboard.test.ts @@ -118,7 +118,7 @@ describe("buildReferrerLeaderboardRevShareLimit", () => { }); describe("Scenario A — unqualified referrer: no award claimed", () => { - it("accumulates uncapped award but cappedAwardValue is $0 when not qualified", () => { + it("accumulates uncapped award but cappedAward is $0 when not qualified", () => { // Half a year of duration → base revenue = $2.50 (< $5 threshold) const events = [makeEvent(ADDR_A, 1000, Math.floor(SECONDS_PER_YEAR / 2))]; const rules = buildTestRules(); @@ -128,9 +128,9 @@ describe("buildReferrerLeaderboardRevShareLimit", () => { expect(referrer).toBeDefined(); expect(referrer.isQualified).toBe(false); - // uncappedAwardValue = 0.5 × ($5 × 0.5 years) = 0.5 × $2.50 = $1.25 - expect(referrer.uncappedAwardValue.amount).toBe(parseUsdc("1.25").amount); - expect(referrer.cappedAwardValue.amount).toBe(0n); + // uncappedAward = 0.5 × ($5 × 0.5 years) = 0.5 × $2.50 = $1.25 + expect(referrer.uncappedAward.amount).toBe(parseUsdc("1.25").amount); + expect(referrer.cappedAward.amount).toBe(0n); // Pool should be fully intact expect(result.aggregatedMetrics.awardPoolRemaining.amount).toBe(rules.awardPool.amount); @@ -152,14 +152,14 @@ describe("buildReferrerLeaderboardRevShareLimit", () => { const referrer = result.referrers.get(ADDR_A)!; expect(referrer.isQualified).toBe(true); - expect(referrer.uncappedAwardValue.amount).toBe(UNCAPPED_AWARD_1Y.amount); + expect(referrer.uncappedAward.amount).toBe(UNCAPPED_AWARD_1Y.amount); // Claims all accumulated: 2 × $1.25 = $2.50 - expect(referrer.cappedAwardValue.amount).toBe(UNCAPPED_AWARD_1Y.amount); + expect(referrer.cappedAward.amount).toBe(UNCAPPED_AWARD_1Y.amount); }); }); describe("Scenario B-2 — just qualifies, but pool is too small to cover full accumulated award", () => { - it("cappedAwardValue is capped by remaining pool when qualifying", () => { + it("cappedAward is capped by remaining pool when qualifying", () => { // Same as Scenario B but pool only has $1.50 const poolAmount = parseUsdc("1.5"); const rules = buildTestRules(poolAmount); @@ -172,10 +172,10 @@ describe("buildReferrerLeaderboardRevShareLimit", () => { const referrer = result.referrers.get(ADDR_A)!; expect(referrer.isQualified).toBe(true); - // uncappedAwardValue = $2.50 (uncapped) - expect(referrer.uncappedAwardValue.amount).toBe(UNCAPPED_AWARD_1Y.amount); - // cappedAwardValue capped at $1.50 (pool limit) - expect(referrer.cappedAwardValue.amount).toBe(poolAmount.amount); + // uncappedAward = $2.50 (uncapped) + expect(referrer.uncappedAward.amount).toBe(UNCAPPED_AWARD_1Y.amount); + // cappedAward capped at $1.50 (pool limit) + expect(referrer.cappedAward.amount).toBe(poolAmount.amount); // Pool fully depleted expect(result.aggregatedMetrics.awardPoolRemaining.amount).toBe(0n); }); @@ -196,15 +196,15 @@ describe("buildReferrerLeaderboardRevShareLimit", () => { const referrer = result.referrers.get(ADDR_A)!; expect(referrer.isQualified).toBe(true); - // uncappedAwardValue = 0.5 × (2 × $5) = $5.00 - expect(referrer.uncappedAwardValue.amount).toBe(parseUsdc("5").amount); - // cappedAwardValue = $2.50 (qualifying) + $2.50 (incremental) = $5.00 - expect(referrer.cappedAwardValue.amount).toBe(parseUsdc("5").amount); + // uncappedAward = 0.5 × (2 × $5) = $5.00 + expect(referrer.uncappedAward.amount).toBe(parseUsdc("5").amount); + // cappedAward = $2.50 (qualifying) + $2.50 (incremental) = $5.00 + expect(referrer.cappedAward.amount).toBe(parseUsdc("5").amount); }); }); describe("Scenario C-2 — already qualified, pool only partially covers incremental award", () => { - it("cappedAwardValue is partially truncated on subsequent event when pool is nearly empty", () => { + it("cappedAward is partially truncated on subsequent event when pool is nearly empty", () => { // Pool = $3.00 // Event 1 at t=1000: 1 year → qualifies, claim min($2.50, $3.00) = $2.50, pool = $0.50 // Event 2 at t=2000: 1 year → already qualified, incremental $2.50, claim min($2.50, $0.50) = $0.50, pool = $0 @@ -218,10 +218,10 @@ describe("buildReferrerLeaderboardRevShareLimit", () => { const referrer = result.referrers.get(ADDR_A)!; expect(referrer.isQualified).toBe(true); - // uncappedAwardValue = 0.5 × $10 = $5.00 (uncapped) - expect(referrer.uncappedAwardValue.amount).toBe(parseUsdc("5").amount); - // cappedAwardValue = $2.50 + $0.50 = $3.00 (capped at pool) - expect(referrer.cappedAwardValue.amount).toBe(parseUsdc("3").amount); + // uncappedAward = 0.5 × $10 = $5.00 (uncapped) + expect(referrer.uncappedAward.amount).toBe(parseUsdc("5").amount); + // cappedAward = $2.50 + $0.50 = $3.00 (capped at pool) + expect(referrer.cappedAward.amount).toBe(parseUsdc("3").amount); // Pool fully depleted expect(result.aggregatedMetrics.awardPoolRemaining.amount).toBe(0n); }); @@ -237,8 +237,8 @@ describe("buildReferrerLeaderboardRevShareLimit", () => { const referrer = result.referrers.get(ADDR_A)!; expect(referrer.isQualified).toBe(true); - expect(referrer.uncappedAwardValue.amount).toBe(UNCAPPED_AWARD_1Y.amount); - expect(referrer.cappedAwardValue.amount).toBe(0n); + expect(referrer.uncappedAward.amount).toBe(UNCAPPED_AWARD_1Y.amount); + expect(referrer.cappedAward.amount).toBe(0n); }); }); @@ -258,16 +258,16 @@ describe("buildReferrerLeaderboardRevShareLimit", () => { const referrerB = result.referrers.get(ADDR_B)!; expect(referrerA.isQualified).toBe(true); - expect(referrerA.cappedAwardValue.amount).toBe(UNCAPPED_AWARD_1Y.amount); // $2.50 + expect(referrerA.cappedAward.amount).toBe(UNCAPPED_AWARD_1Y.amount); // $2.50 expect(referrerB.isQualified).toBe(true); - expect(referrerB.cappedAwardValue.amount).toBe(parseUsdc("1.5").amount); // $1.50 (only remaining) + expect(referrerB.cappedAward.amount).toBe(parseUsdc("1.5").amount); // $1.50 (only remaining) // Pool fully depleted expect(result.aggregatedMetrics.awardPoolRemaining.amount).toBe(0n); }); - it("referrer who qualifies after pool is empty gets $0 cappedAwardValue", () => { + it("referrer who qualifies after pool is empty gets $0 cappedAward", () => { // Pool = $2.50 (only enough for 1 qualifying referrer) // ReferrerA qualifies at t=1000, claims $2.50, pool = $0 // ReferrerB qualifies at t=2000, claims min($2.50, $0) = $0 @@ -281,8 +281,8 @@ describe("buildReferrerLeaderboardRevShareLimit", () => { const referrerA = result.referrers.get(ADDR_A)!; const referrerB = result.referrers.get(ADDR_B)!; - expect(referrerA.cappedAwardValue.amount).toBe(UNCAPPED_AWARD_1Y.amount); // $2.50 - expect(referrerB.cappedAwardValue.amount).toBe(0n); // $0 — pool empty + expect(referrerA.cappedAward.amount).toBe(UNCAPPED_AWARD_1Y.amount); // $2.50 + expect(referrerB.cappedAward.amount).toBe(0n); // $0 — pool empty expect(result.aggregatedMetrics.awardPoolRemaining.amount).toBe(0n); }); @@ -304,12 +304,12 @@ describe("buildReferrerLeaderboardRevShareLimit", () => { const referrerC = result.referrers.get(ADDR_C)!; // Non-truncated: full uncapped award - expect(referrerA.cappedAwardValue.amount).toBe(UNCAPPED_AWARD_1Y.amount); + expect(referrerA.cappedAward.amount).toBe(UNCAPPED_AWARD_1Y.amount); // Partially truncated: less than uncapped but > 0 - expect(referrerB.cappedAwardValue.amount).toBeGreaterThan(0n); - expect(referrerB.cappedAwardValue.amount).toBeLessThan(UNCAPPED_AWARD_1Y.amount); + expect(referrerB.cappedAward.amount).toBeGreaterThan(0n); + expect(referrerB.cappedAward.amount).toBeLessThan(UNCAPPED_AWARD_1Y.amount); // Fully truncated: pool empty - expect(referrerC.cappedAwardValue.amount).toBe(0n); + expect(referrerC.cappedAward.amount).toBe(0n); expect(result.aggregatedMetrics.awardPoolRemaining.amount).toBe(0n); }); }); @@ -331,13 +331,13 @@ describe("buildReferrerLeaderboardRevShareLimit", () => { const result = buildReferrerLeaderboardRevShareLimit(events, rules, accurateAsOf); // ADDR_A has the lower (earlier) id, should claim the pool first - expect(result.referrers.get(ADDR_A)!.cappedAwardValue.amount).toBe(UNCAPPED_AWARD_1Y.amount); - expect(result.referrers.get(ADDR_B)!.cappedAwardValue.amount).toBe(0n); + expect(result.referrers.get(ADDR_A)!.cappedAward.amount).toBe(UNCAPPED_AWARD_1Y.amount); + expect(result.referrers.get(ADDR_B)!.cappedAward.amount).toBe(0n); }); }); describe("Ranking", () => { - it("ranks referrers by cappedAwardValue desc, then uncappedAwardValue desc", () => { + it("ranks referrers by cappedAward desc, then uncappedAward desc", () => { // Pool = $1000 (unlimited for this test) // ADDR_A: 1 year → qualifies at t=1000, cappedAward = $2.50, uncappedAward = $2.50 // ADDR_B: 2 years → qualifies at t=2000, cappedAward = $5.00, uncappedAward = $5.00 @@ -359,8 +359,8 @@ describe("buildReferrerLeaderboardRevShareLimit", () => { expect(result.referrers.get(ADDR_C)!.rank).toBe(3); }); - it("two fully-truncated referrers are ranked by uncappedAwardValue desc", () => { - // Pool = $0 — implementation sorts by totalIncrementalDuration desc, equivalent to uncappedAwardValue desc here. + it("two fully-truncated referrers are ranked by uncappedAward desc", () => { + // Pool = $0 — implementation sorts by totalIncrementalDuration desc, equivalent to uncappedAward desc here. // ADDR_A: 2 years → qualifies, uncappedAward = $5.00, cappedAward = $0 // ADDR_B: 1 year → qualifies, uncappedAward = $2.50, cappedAward = $0 const rules = buildTestRules(priceUsdc(0n)); @@ -431,14 +431,14 @@ describe("buildReferrerLeaderboardRevShareLimit", () => { // ADDR_A: 1 year at $10/yr → $10 base → uncapped = 0.5 × $10 = $5.00 expect(referrerA.isQualified).toBe(true); - expect(referrerA.uncappedAwardValue.amount).toBe(parseUsdc("5").amount); - expect(referrerA.cappedAwardValue.amount).toBe(parseUsdc("5").amount); + expect(referrerA.uncappedAward.amount).toBe(parseUsdc("5").amount); + expect(referrerA.cappedAward.amount).toBe(parseUsdc("5").amount); // ADDR_B: 0.5 years at $10/yr → $5 base → below $10 threshold → not qualified // uncapped = 0.5 × $5 = $2.50, but capped = $0 expect(referrerB.isQualified).toBe(false); - expect(referrerB.uncappedAwardValue.amount).toBe(parseUsdc("2.5").amount); - expect(referrerB.cappedAwardValue.amount).toBe(0n); + expect(referrerB.uncappedAward.amount).toBe(parseUsdc("2.5").amount); + expect(referrerB.cappedAward.amount).toBe(0n); // Pool consumed only by ADDR_A's $5 claim expect(result.aggregatedMetrics.awardPoolRemaining.amount).toBe(parseUsdc("995").amount); @@ -476,14 +476,14 @@ describe("buildReferrerLeaderboardRevShareLimit", () => { expect(referrerA.isQualified).toBe(true); expect(referrerA.isAdminDisqualified).toBe(false); expect(referrerA.adminDisqualificationReason).toBe(null); - expect(referrerA.cappedAwardValue.amount).toBe(UNCAPPED_AWARD_1Y.amount); + expect(referrerA.cappedAward.amount).toBe(UNCAPPED_AWARD_1Y.amount); expect(referrerB.isQualified).toBe(true); expect(referrerB.isAdminDisqualified).toBe(false); expect(referrerB.adminDisqualificationReason).toBe(null); }); - it("disqualified referrer who met threshold: cappedAwardValue = 0, pool preserved for next", () => { + it("disqualified referrer who met threshold: cappedAward = 0, pool preserved for next", () => { // ADDR_A qualifies by revenue but is admin-disqualified → pool claim = 0 // ADDR_B qualifies later → gets the full pool share const rules = buildTestRules(parseUsdc("1000"), parseUsdc("5"), [ @@ -501,12 +501,12 @@ describe("buildReferrerLeaderboardRevShareLimit", () => { expect(referrerA.isAdminDisqualified).toBe(true); expect(referrerA.adminDisqualificationReason).toBe("self-referral"); expect(referrerA.isQualified).toBe(false); - expect(referrerA.cappedAwardValue.amount).toBe(0n); + expect(referrerA.cappedAward.amount).toBe(0n); // Pool was not consumed by ADDR_A, so ADDR_B gets the full award expect(referrerB.isQualified).toBe(true); expect(referrerB.isAdminDisqualified).toBe(false); - expect(referrerB.cappedAwardValue.amount).toBe(UNCAPPED_AWARD_1Y.amount); + expect(referrerB.cappedAward.amount).toBe(UNCAPPED_AWARD_1Y.amount); }); it("disqualified referrer who never met the revenue threshold: pool unchanged", () => { @@ -522,7 +522,7 @@ describe("buildReferrerLeaderboardRevShareLimit", () => { expect(referrerA.isAdminDisqualified).toBe(true); expect(referrerA.adminDisqualificationReason).toBe("promoting discounts"); expect(referrerA.isQualified).toBe(false); - expect(referrerA.cappedAwardValue.amount).toBe(0n); + expect(referrerA.cappedAward.amount).toBe(0n); // Pool fully intact expect(result.aggregatedMetrics.awardPoolRemaining.amount).toBe(parseUsdc("1000").amount); }); @@ -553,18 +553,18 @@ describe("buildReferrerLeaderboardRevShareLimit", () => { expect(referrerB.rank).toBe(1); expect(referrerB.isQualified).toBe(true); expect(referrerB.isAdminDisqualified).toBe(false); - expect(referrerB.cappedAwardValue.amount).toBe(UNCAPPED_AWARD_1Y.amount); + expect(referrerB.cappedAward.amount).toBe(UNCAPPED_AWARD_1Y.amount); expect(referrerA.rank).toBe(2); expect(referrerA.isAdminDisqualified).toBe(true); expect(referrerA.adminDisqualificationReason).toBe("cheating"); expect(referrerA.isQualified).toBe(false); - expect(referrerA.cappedAwardValue.amount).toBe(0n); + expect(referrerA.cappedAward.amount).toBe(0n); expect(referrerC.rank).toBe(3); expect(referrerC.isQualified).toBe(false); expect(referrerC.isAdminDisqualified).toBe(false); - expect(referrerC.cappedAwardValue.amount).toBe(0n); + expect(referrerC.cappedAward.amount).toBe(0n); }); it("multiple disqualifications: all disqualified referrers get isAdminDisqualified=true", () => { @@ -586,17 +586,17 @@ describe("buildReferrerLeaderboardRevShareLimit", () => { expect(referrerA.isAdminDisqualified).toBe(true); expect(referrerA.adminDisqualificationReason).toBe("reason-a"); expect(referrerA.isQualified).toBe(false); - expect(referrerA.cappedAwardValue.amount).toBe(0n); + expect(referrerA.cappedAward.amount).toBe(0n); expect(referrerB.isAdminDisqualified).toBe(true); expect(referrerB.adminDisqualificationReason).toBe("reason-b"); expect(referrerB.isQualified).toBe(false); - expect(referrerB.cappedAwardValue.amount).toBe(0n); + expect(referrerB.cappedAward.amount).toBe(0n); expect(referrerC.isAdminDisqualified).toBe(false); expect(referrerC.adminDisqualificationReason).toBe(null); expect(referrerC.isQualified).toBe(true); - expect(referrerC.cappedAwardValue.amount).toBe(UNCAPPED_AWARD_1Y.amount); + expect(referrerC.cappedAward.amount).toBe(UNCAPPED_AWARD_1Y.amount); }); it("duplicate address in disqualifications: buildReferralProgramRulesRevShareLimit throws", () => { diff --git a/packages/ens-referrals/src/v1/award-models/rev-share-limit/leaderboard.ts b/packages/ens-referrals/src/v1/award-models/rev-share-limit/leaderboard.ts index f3039558c5..d85abd45fc 100644 --- a/packages/ens-referrals/src/v1/award-models/rev-share-limit/leaderboard.ts +++ b/packages/ens-referrals/src/v1/award-models/rev-share-limit/leaderboard.ts @@ -167,7 +167,7 @@ export const buildReferrerLeaderboardRevShareLimit = ( } // 3. Sort referrers to assign ranks: - // 1. cappedAwardValue desc — actual pool claims, race winners first + // 1. cappedAward desc — actual pool claims, race winners first // 2. totalIncrementalDuration desc — tie-break for pool-depleted referrers // 3. referrer address desc — deterministic tie-break // Both `a` and `b` are keys from `referrerStates`, so lookups are always defined. @@ -175,7 +175,7 @@ export const buildReferrerLeaderboardRevShareLimit = ( const stateA = referrerStates.get(a) as ReferrerRaceState; const stateB = referrerStates.get(b) as ReferrerRaceState; - // Primary: cappedAwardValue desc (bigint comparison) + // Primary: cappedAward desc (bigint comparison) if (stateB.cappedAwardAmount !== stateA.cappedAwardAmount) { return stateB.cappedAwardAmount > stateA.cappedAwardAmount ? 1 : -1; } @@ -213,14 +213,14 @@ export const buildReferrerLeaderboardRevShareLimit = ( rules, ); - const uncappedAwardValue = scalePrice( + const uncappedAward = scalePrice( revShareMetrics.totalBaseRevenueContribution, rules.maxBaseRevenueShare, ); return buildAwardedReferrerMetricsRevShareLimit( rankedMetrics, - uncappedAwardValue, + uncappedAward, priceUsdc(state.cappedAwardAmount), rules, ); diff --git a/packages/ens-referrals/src/v1/award-models/rev-share-limit/metrics.ts b/packages/ens-referrals/src/v1/award-models/rev-share-limit/metrics.ts index 3e913bddf4..75bb69f6f6 100644 --- a/packages/ens-referrals/src/v1/award-models/rev-share-limit/metrics.ts +++ b/packages/ens-referrals/src/v1/award-models/rev-share-limit/metrics.ts @@ -72,7 +72,7 @@ export interface RankedReferrerMetricsRevShareLimit extends ReferrerMetricsRevSh * Identifies if the referrer is eligible for an award under the {@link ReferralProgramRulesRevShareLimit}. * * Note: this is a purely rule-based eligibility predicate and does NOT guarantee - * `cappedAwardValue.amount > 0n` — a qualified referrer may still receive $0 if the + * `cappedAward.amount > 0n` — a qualified referrer may still receive $0 if the * award pool is already depleted by earlier referrers in the race. * * @invariant true if and only if `totalBaseRevenueContribution` is greater than or equal to @@ -157,30 +157,30 @@ export const buildRankedReferrerMetricsRevShareLimit = ( }; /** - * Extends {@link RankedReferrerMetricsRevShareLimit} with approximate award value. + * Extends {@link RankedReferrerMetricsRevShareLimit} with the referrer's uncapped and capped awards. */ export interface AwardedReferrerMetricsRevShareLimit extends RankedReferrerMetricsRevShareLimit { /** - * The uncapped USDC award value for this referrer, computed as + * The uncapped USDC award for this referrer, computed as * `maxBaseRevenueShare × totalBaseRevenueContribution`. * * Represents what the referrer would receive if the pool were unlimited and the referrer were qualified. * Independent of the pool state, qualification status, and admin disqualification status. */ - uncappedAwardValue: PriceUsdc; + uncappedAward: PriceUsdc; /** - * The USDC value of the referrer's (tentative) award. + * The referrer's (tentative) capped USDC award. * * This is the amount actually claimed from the pool by this referrer, capped by * the remaining pool at the time of their qualifying referrals. * * @invariant Guaranteed to be a valid PriceUsdc with amount between 0 and {@link ReferralProgramRulesRevShareLimit.awardPool.amount} (inclusive) - * @invariant Always <= uncappedAwardValue.amount + * @invariant Always <= uncappedAward.amount * @invariant Amount equal to 0 when {@link isAdminDisqualified} is true. * @invariant Amount equal to 0 when {@link isQualified} is false. */ - cappedAwardValue: PriceUsdc; + cappedAward: PriceUsdc; } export const validateAwardedReferrerMetricsRevShareLimit = ( @@ -189,49 +189,47 @@ export const validateAwardedReferrerMetricsRevShareLimit = ( ): void => { validateRankedReferrerMetricsRevShareLimit(metrics, rules); - makePriceUsdcSchema("AwardedReferrerMetricsRevShareLimit.uncappedAwardValue").parse( - metrics.uncappedAwardValue, + makePriceUsdcSchema("AwardedReferrerMetricsRevShareLimit.uncappedAward").parse( + metrics.uncappedAward, ); - makePriceUsdcSchema("AwardedReferrerMetricsRevShareLimit.cappedAwardValue").parse( - metrics.cappedAwardValue, - ); + makePriceUsdcSchema("AwardedReferrerMetricsRevShareLimit.cappedAward").parse(metrics.cappedAward); - if (metrics.isAdminDisqualified && metrics.cappedAwardValue.amount !== 0n) { + if (metrics.isAdminDisqualified && metrics.cappedAward.amount !== 0n) { throw new Error( - `AwardedReferrerMetricsRevShareLimit: cappedAwardValue.amount must be 0n for admin-disqualified referrers, got ${metrics.cappedAwardValue.amount.toString()}.`, + `AwardedReferrerMetricsRevShareLimit: cappedAward.amount must be 0n for admin-disqualified referrers, got ${metrics.cappedAward.amount.toString()}.`, ); } - if (!metrics.isQualified && metrics.cappedAwardValue.amount !== 0n) { + if (!metrics.isQualified && metrics.cappedAward.amount !== 0n) { throw new Error( - `AwardedReferrerMetricsRevShareLimit: cappedAwardValue.amount must be 0n for unqualified referrers, got ${metrics.cappedAwardValue.amount.toString()}.`, + `AwardedReferrerMetricsRevShareLimit: cappedAward.amount must be 0n for unqualified referrers, got ${metrics.cappedAward.amount.toString()}.`, ); } - if (metrics.cappedAwardValue.amount > rules.awardPool.amount) { + if (metrics.cappedAward.amount > rules.awardPool.amount) { throw new Error( - `AwardedReferrerMetricsRevShareLimit: cappedAwardValue.amount ${metrics.cappedAwardValue.amount.toString()} exceeds awardPool.amount ${rules.awardPool.amount.toString()}.`, + `AwardedReferrerMetricsRevShareLimit: cappedAward.amount ${metrics.cappedAward.amount.toString()} exceeds awardPool.amount ${rules.awardPool.amount.toString()}.`, ); } - if (metrics.cappedAwardValue.amount > metrics.uncappedAwardValue.amount) { + if (metrics.cappedAward.amount > metrics.uncappedAward.amount) { throw new Error( - `AwardedReferrerMetricsRevShareLimit: cappedAwardValue.amount ${metrics.cappedAwardValue.amount.toString()} exceeds uncappedAwardValue.amount ${metrics.uncappedAwardValue.amount.toString()}.`, + `AwardedReferrerMetricsRevShareLimit: cappedAward.amount ${metrics.cappedAward.amount.toString()} exceeds uncappedAward.amount ${metrics.uncappedAward.amount.toString()}.`, ); } }; export const buildAwardedReferrerMetricsRevShareLimit = ( referrer: RankedReferrerMetricsRevShareLimit, - uncappedAwardValue: PriceUsdc, - cappedAwardValue: PriceUsdc, + uncappedAward: PriceUsdc, + cappedAward: PriceUsdc, rules: ReferralProgramRulesRevShareLimit, ): AwardedReferrerMetricsRevShareLimit => { const result = { ...referrer, - uncappedAwardValue, - cappedAwardValue, + uncappedAward, + cappedAward, } satisfies AwardedReferrerMetricsRevShareLimit; validateAwardedReferrerMetricsRevShareLimit(result, rules); @@ -317,21 +315,21 @@ export const validateUnrankedReferrerMetricsRevShareLimit = ( ); } - makePriceUsdcSchema("UnrankedReferrerMetricsRevShareLimit.uncappedAwardValue").parse( - metrics.uncappedAwardValue, + makePriceUsdcSchema("UnrankedReferrerMetricsRevShareLimit.uncappedAward").parse( + metrics.uncappedAward, ); - if (metrics.uncappedAwardValue.amount !== 0n) { + if (metrics.uncappedAward.amount !== 0n) { throw new Error( - `Invalid UnrankedReferrerMetricsRevShareLimit: uncappedAwardValue.amount must be 0n, got: ${metrics.uncappedAwardValue.amount.toString()}.`, + `Invalid UnrankedReferrerMetricsRevShareLimit: uncappedAward.amount must be 0n, got: ${metrics.uncappedAward.amount.toString()}.`, ); } - makePriceUsdcSchema("UnrankedReferrerMetricsRevShareLimit.cappedAwardValue").parse( - metrics.cappedAwardValue, + makePriceUsdcSchema("UnrankedReferrerMetricsRevShareLimit.cappedAward").parse( + metrics.cappedAward, ); - if (metrics.cappedAwardValue.amount !== 0n) { + if (metrics.cappedAward.amount !== 0n) { throw new Error( - `Invalid UnrankedReferrerMetricsRevShareLimit: cappedAwardValue.amount must be 0n, got: ${metrics.cappedAwardValue.amount.toString()}.`, + `Invalid UnrankedReferrerMetricsRevShareLimit: cappedAward.amount must be 0n, got: ${metrics.cappedAward.amount.toString()}.`, ); } }; @@ -353,8 +351,8 @@ export const buildUnrankedReferrerMetricsRevShareLimit = ( totalBaseRevenueContribution: priceUsdc(0n), rank: null, isQualified: false, - uncappedAwardValue: priceUsdc(0n), - cappedAwardValue: priceUsdc(0n), + uncappedAward: priceUsdc(0n), + cappedAward: priceUsdc(0n), isAdminDisqualified: disqualification !== null, adminDisqualificationReason: disqualification?.reason ?? null, } satisfies UnrankedReferrerMetricsRevShareLimit; From 596c49744ec3773548aa82e50d4f0c7964a5d76c Mon Sep 17 00:00:00 2001 From: Goader Date: Wed, 8 Apr 2026 17:33:29 +0200 Subject: [PATCH 10/11] review applied --- .../rev-share-limit/api/zod-schemas.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/ens-referrals/src/v1/award-models/rev-share-limit/api/zod-schemas.ts b/packages/ens-referrals/src/v1/award-models/rev-share-limit/api/zod-schemas.ts index f6ddf361a1..0d55dfb512 100644 --- a/packages/ens-referrals/src/v1/award-models/rev-share-limit/api/zod-schemas.ts +++ b/packages/ens-referrals/src/v1/award-models/rev-share-limit/api/zod-schemas.ts @@ -284,10 +284,14 @@ export const makeReferrerLeaderboardPageRevShareLimitSchema = ( message: `${valueLabel}.awardModel must equal ${valueLabel}.rules.awardModel`, path: ["awardModel"], }) - .refine( - (data) => data.referrers.every((r) => r.cappedAward.amount <= data.rules.awardPool.amount), - { - message: `${valueLabel}.referrers[].cappedAward must be <= ${valueLabel}.rules.awardPool`, - path: ["referrers"], - }, - ); + .superRefine((data, ctx) => { + data.referrers.forEach((referrer, index) => { + if (referrer.cappedAward.amount > data.rules.awardPool.amount) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `${valueLabel}.referrers[${index}].cappedAward must be <= ${valueLabel}.rules.awardPool`, + path: ["referrers", index, "cappedAward", "amount"], + }); + } + }); + }); From 7aee7f13634d5048bb50f794a8c8f5f584b57e70 Mon Sep 17 00:00:00 2001 From: Goader Date: Thu, 9 Apr 2026 15:07:46 +0200 Subject: [PATCH 11/11] review --- .../rev-share-limit/leaderboard.ts | 36 +++++++------------ .../award-models/rev-share-limit/metrics.ts | 10 +++--- .../v1/award-models/rev-share-limit/rules.ts | 8 ++--- 3 files changed, 22 insertions(+), 32 deletions(-) diff --git a/packages/ens-referrals/src/v1/award-models/rev-share-limit/leaderboard.ts b/packages/ens-referrals/src/v1/award-models/rev-share-limit/leaderboard.ts index d85abd45fc..501bc617cb 100644 --- a/packages/ens-referrals/src/v1/award-models/rev-share-limit/leaderboard.ts +++ b/packages/ens-referrals/src/v1/award-models/rev-share-limit/leaderboard.ts @@ -86,11 +86,11 @@ interface ReferrerRaceState { * race algorithm over individual referral events. * * Events are processed in chronological order. When a referrer first crosses the qualification - * threshold, they claim ALL accumulated uncapped award at once (capped by remaining pool). - * After qualifying, each subsequent event claims that event's incremental uncapped award (also - * capped). Once the pool reaches $0, no further awards are issued to anyone. + * threshold, they claim ALL accumulated uncapped awards at once (capped by remaining award pool). + * After qualifying, each referrer's subsequent referrals claim that event's incremental capped award. + * Once the award pool is exhausted, no further awards are issued to anyone. * - * @param events - Raw referral events from the database (unsorted; will be sorted internally). + * @param events - Raw referral events from ENSDb (unsorted; will be sorted internally). * @param rules - The {@link ReferralProgramRulesRevShareLimit} defining the program parameters. * @param accurateAsOf - Timestamp indicating data freshness. */ @@ -104,7 +104,7 @@ export const buildReferrerLeaderboardRevShareLimit = ( // 2. Process events sequentially to run the race. const referrerStates = new Map(); - let poolRemainingAmount = rules.awardPool.amount; + let awardPoolRemaining = rules.awardPool.amount; for (const event of sortedEvents) { const referrer = normalizeAddress(event.referrer); @@ -146,9 +146,9 @@ export const buildReferrerLeaderboardRevShareLimit = ( priceUsdc(totalBaseRevenueAmount), rules.maxBaseRevenueShare, ).amount; - const incrementalCappedAward = bigintMin(accumulatedUncappedAward, poolRemainingAmount); + const incrementalCappedAward = bigintMin(accumulatedUncappedAward, awardPoolRemaining); state.cappedAwardAmount += incrementalCappedAward; - poolRemainingAmount -= incrementalCappedAward; + awardPoolRemaining -= incrementalCappedAward; state.wasQualified = true; } else if (state.wasQualified) { // Already qualified: claim this event's incremental uncapped award. @@ -159,9 +159,9 @@ export const buildReferrerLeaderboardRevShareLimit = ( priceUsdc(incrementalBaseRevenueAmount), rules.maxBaseRevenueShare, ).amount; - const incrementalCappedAward = bigintMin(incrementalUncappedAward, poolRemainingAmount); + const incrementalCappedAward = bigintMin(incrementalUncappedAward, awardPoolRemaining); state.cappedAwardAmount += incrementalCappedAward; - poolRemainingAmount -= incrementalCappedAward; + awardPoolRemaining -= incrementalCappedAward; } // If not yet qualified, nothing is claimed from the pool. } @@ -170,11 +170,7 @@ export const buildReferrerLeaderboardRevShareLimit = ( // 1. cappedAward desc — actual pool claims, race winners first // 2. totalIncrementalDuration desc — tie-break for pool-depleted referrers // 3. referrer address desc — deterministic tie-break - // Both `a` and `b` are keys from `referrerStates`, so lookups are always defined. - const sortedAddresses = [...referrerStates.keys()].sort((a, b) => { - const stateA = referrerStates.get(a) as ReferrerRaceState; - const stateB = referrerStates.get(b) as ReferrerRaceState; - + const sortedEntries = [...referrerStates.entries()].sort(([a, stateA], [b, stateB]) => { // Primary: cappedAward desc (bigint comparison) if (stateB.cappedAwardAmount !== stateA.cappedAwardAmount) { return stateB.cappedAwardAmount > stateA.cappedAwardAmount ? 1 : -1; @@ -192,12 +188,8 @@ export const buildReferrerLeaderboardRevShareLimit = ( }); // 4. Build AwardedReferrerMetricsRevShareLimit for each referrer. - const awardedReferrers: AwardedReferrerMetricsRevShareLimit[] = sortedAddresses.map( - (referrerAddr, index) => { - // `sortedAddresses` is derived directly from `referrerStates.keys()`, so - // the state entry is always present. - const state = referrerStates.get(referrerAddr) as ReferrerRaceState; - + const awardedReferrers: AwardedReferrerMetricsRevShareLimit[] = sortedEntries.map( + ([referrerAddr, state], index) => { const baseMetrics = buildReferrerMetrics( referrerAddr, state.totalReferrals, @@ -227,11 +219,9 @@ export const buildReferrerLeaderboardRevShareLimit = ( }, ); - const awardPoolRemaining = priceUsdc(poolRemainingAmount); - const aggregatedMetrics = buildAggregatedReferrerMetricsRevShareLimit( awardedReferrers, - awardPoolRemaining, + priceUsdc(awardPoolRemaining), ); const referrers = new Map(awardedReferrers.map((r) => [r.referrer, r])); diff --git a/packages/ens-referrals/src/v1/award-models/rev-share-limit/metrics.ts b/packages/ens-referrals/src/v1/award-models/rev-share-limit/metrics.ts index 75bb69f6f6..64af50dfd9 100644 --- a/packages/ens-referrals/src/v1/award-models/rev-share-limit/metrics.ts +++ b/packages/ens-referrals/src/v1/award-models/rev-share-limit/metrics.ts @@ -16,7 +16,7 @@ import { isReferrerQualifiedRevShareLimit, type ReferralProgramRulesRevShareLimi export interface ReferrerMetricsRevShareLimit extends ReferrerMetrics { /** * The referrer's base revenue contribution - * (base-fee-only: `rules.baseAnnualRevenueContribution` × years of incremental duration). + * (`rules.baseAnnualRevenueContribution` × years of incremental duration). * Used for qualification and award calculation in the rev-share-limit model. * * @invariant Guaranteed to be `priceUsdc(rules.baseAnnualRevenueContribution.amount * BigInt(totalIncrementalDuration) / BigInt(SECONDS_PER_YEAR))` @@ -73,7 +73,7 @@ export interface RankedReferrerMetricsRevShareLimit extends ReferrerMetricsRevSh * * Note: this is a purely rule-based eligibility predicate and does NOT guarantee * `cappedAward.amount > 0n` — a qualified referrer may still receive $0 if the - * award pool is already depleted by earlier referrers in the race. + * capped award pool is already exhausted by earlier referrers in the race. * * @invariant true if and only if `totalBaseRevenueContribution` is greater than or equal to * {@link ReferralProgramRulesRevShareLimit.minBaseRevenueContribution} AND @@ -164,7 +164,7 @@ export interface AwardedReferrerMetricsRevShareLimit extends RankedReferrerMetri * The uncapped USDC award for this referrer, computed as * `maxBaseRevenueShare × totalBaseRevenueContribution`. * - * Represents what the referrer would receive if the pool were unlimited and the referrer were qualified. + * Represents what the referrer would receive if the pool were uncapped and the referrer were qualified. * Independent of the pool state, qualification status, and admin disqualification status. */ uncappedAward: PriceUsdc; @@ -172,8 +172,8 @@ export interface AwardedReferrerMetricsRevShareLimit extends RankedReferrerMetri /** * The referrer's (tentative) capped USDC award. * - * This is the amount actually claimed from the pool by this referrer, capped by - * the remaining pool at the time of their qualifying referrals. + * This is the amount (tentatively) claimed from the award pool by this referrer, capped by + * the remaining award pool at the time of their qualifying referrals. * * @invariant Guaranteed to be a valid PriceUsdc with amount between 0 and {@link ReferralProgramRulesRevShareLimit.awardPool.amount} (inclusive) * @invariant Always <= uncappedAward.amount diff --git a/packages/ens-referrals/src/v1/award-models/rev-share-limit/rules.ts b/packages/ens-referrals/src/v1/award-models/rev-share-limit/rules.ts index f5d5a2813b..16f49b2095 100644 --- a/packages/ens-referrals/src/v1/award-models/rev-share-limit/rules.ts +++ b/packages/ens-referrals/src/v1/award-models/rev-share-limit/rules.ts @@ -35,7 +35,7 @@ export interface ReferralProgramRulesRevShareLimit extends BaseReferralProgramRu * * In rev-share-limit, each qualified referrer receives a share of their base revenue * contribution (base-fee-only: `baseAnnualRevenueContribution` × years of incremental duration), - * subject to a pool cap and a minimum qualification threshold. + * subject to the award pool cap and a minimum qualification threshold. */ awardModel: typeof ReferralProgramAwardModels.RevShareLimit; @@ -50,7 +50,7 @@ export interface ReferralProgramRulesRevShareLimit extends BaseReferralProgramRu minBaseRevenueContribution: PriceUsdc; /** - * Base revenue contribution per year of incremental duration in USDC. + * Base revenue contribution in USDC per year of incremental duration from referred registrations and renewals. * * Used in `rev-share-limit` qualification and award calculations: * 1 year of incremental duration → this many USDC of base revenue (base-fee-only, excluding premiums). @@ -58,8 +58,8 @@ export interface ReferralProgramRulesRevShareLimit extends BaseReferralProgramRu baseAnnualRevenueContribution: PriceUsdc; /** - * The fraction of the referrer's base revenue contribution that constitutes their max potential award. - * This is the max that ignores the possibility of the award pool becoming exhausted. + * The fraction of the referrer's base revenue contribution that constitutes their max potential award for each referral. + * This is the max for a referral that ignores the possibility of the referrer not having achieved qualification for awards yet, the referrer being disqualified from awards, or the award pool being exhausted. * * @invariant Guaranteed to be a number between 0 and 1 (inclusive) */