diff --git a/.changeset/bright-foxes-dance.md b/.changeset/bright-foxes-dance.md new file mode 100644 index 0000000000..5ff974fe77 --- /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` → `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/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 b1d724a58e..041a25ff4a 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 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 standard award value: ${detail.referrer.standardAwardValue}`); + 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 82bcff6104..d1649b1144 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,9 +43,10 @@ describe("makeReferralProgramEditionConfigSetArraySchema", () => { displayName: "January 2026", rules: { awardModel: ReferralProgramAwardModels.RevShareLimit, - totalAwardPoolValue: parseUsdc("500"), - minQualifiedRevenueContribution: parseUsdc("10"), - qualifiedRevenueShare: 0.5, + awardPool: parseUsdc("500"), + minBaseRevenueContribution: parseUsdc("10"), + baseAnnualRevenueContribution: parseUsdc("5"), + maxBaseRevenueShare: 0.5, startTime: 1000000, endTime: 2000000, subregistryId, @@ -92,14 +93,16 @@ describe("makeReferralProgramEditionConfigSetArraySchema", () => { const rules = revShareLimit!.rules as { awardModel: typeof ReferralProgramAwardModels.RevShareLimit; - totalAwardPoolValue: { amount: bigint; currency: string }; - minQualifiedRevenueContribution: { amount: bigint; currency: string }; - qualifiedRevenueShare: number; + awardPool: { amount: bigint; currency: string }; + minBaseRevenueContribution: { amount: bigint; currency: string }; + baseAnnualRevenueContribution: { 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.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( revShareLimitEdition.rules.areAwardsDistributed, ); @@ -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,9 +217,10 @@ describe("makeReferrerLeaderboardPageSchema", () => { awardModel: ReferralProgramAwardModels.RevShareLimit, rules: { awardModel: ReferralProgramAwardModels.RevShareLimit, - totalAwardPoolValue: parseUsdc("2000"), - minQualifiedRevenueContribution: parseUsdc("10"), - qualifiedRevenueShare: 0.5, + awardPool: parseUsdc("2000"), + minBaseRevenueContribution: parseUsdc("10"), + baseAnnualRevenueContribution: parseUsdc("5"), + maxBaseRevenueShare: 0.5, startTime: 1000000, endTime: 2000000, subregistryId, @@ -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,9 +328,10 @@ describe("makeReferralProgramEditionSummarySchema", () => { status: ReferralProgramEditionStatuses.Active, rules: { awardModel: ReferralProgramAwardModels.RevShareLimit, - totalAwardPoolValue: parseUsdc("2000"), - minQualifiedRevenueContribution: parseUsdc("10"), - qualifiedRevenueShare: 0.5, + awardPool: parseUsdc("2000"), + minBaseRevenueContribution: parseUsdc("10"), + baseAnnualRevenueContribution: parseUsdc("5"), + maxBaseRevenueShare: 0.5, startTime: 1000000, endTime: 2000000, subregistryId, @@ -423,7 +428,7 @@ describe("makeReferrerEditionMetricsSchema", () => { const pieSplitRules = { awardModel: ReferralProgramAwardModels.PieSplit, - totalAwardPoolValue: parseUsdc("1000"), + awardPool: parseUsdc("1000"), maxQualifiedReferrers: 100, startTime: 1000000, endTime: 2000000, @@ -506,9 +511,10 @@ describe("makeReferrerEditionMetricsSchema", () => { type: ReferrerEditionMetricsTypeIds.Ranked, rules: { awardModel: ReferralProgramAwardModels.RevShareLimit, - totalAwardPoolValue: parseUsdc("2000"), - minQualifiedRevenueContribution: parseUsdc("10"), - qualifiedRevenueShare: 0.5, + awardPool: parseUsdc("2000"), + minBaseRevenueContribution: parseUsdc("10"), + baseAnnualRevenueContribution: parseUsdc("5"), + maxBaseRevenueShare: 0.5, startTime: 1000000, endTime: 2000000, subregistryId, @@ -523,8 +529,8 @@ describe("makeReferrerEditionMetricsSchema", () => { totalBaseRevenueContribution: parseUsdc("150"), rank: 1, isQualified: true, - standardAwardValue: parseUsdc("200"), - awardPoolApproxValue: parseUsdc("200"), + uncappedAward: parseUsdc("200"), + cappedAward: parseUsdc("200"), isAdminDisqualified: false, adminDisqualificationReason: null, }, 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/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 3dfa697e30..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 @@ -34,9 +34,10 @@ export function serializeReferralProgramRulesRevShareLimit( ): SerializedReferralProgramRulesRevShareLimit { return { awardModel: rules.awardModel, - totalAwardPoolValue: serializePriceUsdc(rules.totalAwardPoolValue), - minQualifiedRevenueContribution: serializePriceUsdc(rules.minQualifiedRevenueContribution), - qualifiedRevenueShare: rules.qualifiedRevenueShare, + awardPool: serializePriceUsdc(rules.awardPool), + minBaseRevenueContribution: serializePriceUsdc(rules.minBaseRevenueContribution), + baseAnnualRevenueContribution: serializePriceUsdc(rules.baseAnnualRevenueContribution), + maxBaseRevenueShare: rules.maxBaseRevenueShare, startTime: rules.startTime, endTime: rules.endTime, subregistryId: rules.subregistryId, @@ -74,8 +75,8 @@ export function serializeAwardedReferrerMetricsRevShareLimit( totalBaseRevenueContribution: serializePriceUsdc(metrics.totalBaseRevenueContribution), rank: metrics.rank, isQualified: metrics.isQualified, - standardAwardValue: serializePriceUsdc(metrics.standardAwardValue), - awardPoolApproxValue: serializePriceUsdc(metrics.awardPoolApproxValue), + uncappedAward: serializePriceUsdc(metrics.uncappedAward), + cappedAward: serializePriceUsdc(metrics.cappedAward), isAdminDisqualified: metrics.isAdminDisqualified, adminDisqualificationReason: metrics.adminDisqualificationReason, }; @@ -95,8 +96,8 @@ export function serializeUnrankedReferrerMetricsRevShareLimit( totalBaseRevenueContribution: serializePriceUsdc(metrics.totalBaseRevenueContribution), rank: metrics.rank, isQualified: metrics.isQualified, - standardAwardValue: serializePriceUsdc(metrics.standardAwardValue), - awardPoolApproxValue: serializePriceUsdc(metrics.awardPoolApproxValue), + 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 96807091cf..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 @@ -20,10 +20,11 @@ import type { ReferralProgramRulesRevShareLimit } from "../rules"; export interface SerializedReferralProgramRulesRevShareLimit extends Omit< ReferralProgramRulesRevShareLimit, - "totalAwardPoolValue" | "minQualifiedRevenueContribution" | "rulesUrl" + "awardPool" | "minBaseRevenueContribution" | "baseAnnualRevenueContribution" | "rulesUrl" > { - totalAwardPoolValue: SerializedPriceUsdc; - minQualifiedRevenueContribution: SerializedPriceUsdc; + awardPool: SerializedPriceUsdc; + minBaseRevenueContribution: SerializedPriceUsdc; + baseAnnualRevenueContribution: SerializedPriceUsdc; rulesUrl: string; } @@ -45,15 +46,12 @@ export interface SerializedAggregatedReferrerMetricsRevShareLimit export interface SerializedAwardedReferrerMetricsRevShareLimit extends Omit< AwardedReferrerMetricsRevShareLimit, - | "totalRevenueContribution" - | "totalBaseRevenueContribution" - | "standardAwardValue" - | "awardPoolApproxValue" + "totalRevenueContribution" | "totalBaseRevenueContribution" | "uncappedAward" | "cappedAward" > { totalRevenueContribution: SerializedPriceEth; totalBaseRevenueContribution: SerializedPriceUsdc; - standardAwardValue: SerializedPriceUsdc; - awardPoolApproxValue: SerializedPriceUsdc; + uncappedAward: SerializedPriceUsdc; + cappedAward: SerializedPriceUsdc; } /** @@ -62,15 +60,12 @@ export interface SerializedAwardedReferrerMetricsRevShareLimit export interface SerializedUnrankedReferrerMetricsRevShareLimit extends Omit< UnrankedReferrerMetricsRevShareLimit, - | "totalRevenueContribution" - | "totalBaseRevenueContribution" - | "standardAwardValue" - | "awardPoolApproxValue" + "totalRevenueContribution" | "totalBaseRevenueContribution" | "uncappedAward" | "cappedAward" > { totalRevenueContribution: SerializedPriceEth; totalBaseRevenueContribution: SerializedPriceUsdc; - standardAwardValue: SerializedPriceUsdc; - awardPoolApproxValue: 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 9578357b95..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 @@ -40,13 +40,15 @@ export const makeReferralProgramRulesRevShareLimitSchema = ( ) => makeBaseReferralProgramRulesSchema(valueLabel).safeExtend({ awardModel: z.literal(ReferralProgramAwardModels.RevShareLimit), - totalAwardPoolValue: makePriceUsdcSchema(`${valueLabel}.totalAwardPoolValue`), - minQualifiedRevenueContribution: makePriceUsdcSchema( - `${valueLabel}.minQualifiedRevenueContribution`, + awardPool: makePriceUsdcSchema(`${valueLabel}.awardPool`), + minBaseRevenueContribution: makePriceUsdcSchema(`${valueLabel}.minBaseRevenueContribution`), + baseAnnualRevenueContribution: makePriceUsdcSchema( + `${valueLabel}.baseAnnualRevenueContribution`, + ), + 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 +82,8 @@ export const makeAwardedReferrerMetricsRevShareLimitSchema = ( ), rank: makePositiveIntegerSchema(`${valueLabel}.rank`), isQualified: z.boolean(), - standardAwardValue: makePriceUsdcSchema(`${valueLabel}.standardAwardValue`), - awardPoolApproxValue: makePriceUsdcSchema(`${valueLabel}.awardPoolApproxValue`), + uncappedAward: makePriceUsdcSchema(`${valueLabel}.uncappedAward`), + cappedAward: makePriceUsdcSchema(`${valueLabel}.cappedAward`), isAdminDisqualified: z.boolean(), adminDisqualificationReason: z .string() @@ -89,22 +91,25 @@ 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.cappedAward.amount <= data.uncappedAward.amount, { + message: `${valueLabel}.cappedAward must be <= ${valueLabel}.uncappedAward`, + path: ["cappedAward"], }) .refine( (data) => - !data.isAdminDisqualified || - (data.isQualified === false && data.awardPoolApproxValue.amount === 0n), + !data.isAdminDisqualified || (data.isQualified === false && data.cappedAward.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 cappedAward.amount must be 0`, path: ["isAdminDisqualified"], }, ) .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.cappedAward.amount === 0n, { + message: `${valueLabel}.cappedAward must be 0 when isQualified is false`, + path: ["cappedAward"], }); /** @@ -124,8 +129,8 @@ export const makeUnrankedReferrerMetricsRevShareLimitSchema = ( ), rank: z.null(), isQualified: z.literal(false), - standardAwardValue: makePriceUsdcSchema(`${valueLabel}.standardAwardValue`), - awardPoolApproxValue: makePriceUsdcSchema(`${valueLabel}.awardPoolApproxValue`), + uncappedAward: makePriceUsdcSchema(`${valueLabel}.uncappedAward`), + cappedAward: makePriceUsdcSchema(`${valueLabel}.cappedAward`), isAdminDisqualified: z.boolean(), adminDisqualificationReason: z .string() @@ -133,9 +138,29 @@ 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.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.uncappedAward.amount === 0n, { + message: `${valueLabel}.uncappedAward must be 0 for unranked referrers`, + path: ["uncappedAward"], + }) + .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`, @@ -180,6 +205,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.cappedAward.amount <= data.rules.awardPool.amount, { + message: `${valueLabel}.referrer.cappedAward must be <= ${valueLabel}.rules.awardPool`, + path: ["referrer", "cappedAward", "amount"], }); /** @@ -254,4 +283,15 @@ export const makeReferrerLeaderboardPageRevShareLimitSchema = ( .refine((data) => data.awardModel === data.rules.awardModel, { message: `${valueLabel}.awardModel must equal ${valueLabel}.rules.awardModel`, path: ["awardModel"], + }) + .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"], + }); + } + }); }); 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..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, - * standard award value, and award pool approximate 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 79d92fcd67..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 @@ -36,23 +36,27 @@ const CHECKPOINT_PREFIX = /** * Build test rules. * - * - BASE_REVENUE_CONTRIBUTION_PER_YEAR = $5 USDC - * - qualifiedRevenueShare = 0.5 - * - 1 year of duration → $5 base revenue → $2.50 standard award - * - minQualifiedRevenueContribution = $5 → need exactly 1 year to qualify + * - 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 totalAwardPoolValue - USDC amount for the pool (default: $1000) - * @param minQualifiedRevenueContribution - USDC threshold (default: $5 = 1 year) + * @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( - totalAwardPoolValue = parseUsdc("1000"), - minQualifiedRevenueContribution = parseUsdc("5"), + awardPool = parseUsdc("1000"), + minBaseRevenueContribution = parseUsdc("5"), disqualifications: ReferralProgramEditionDisqualification[] = [], + baseAnnualRevenueContribution = parseUsdc("5"), ) { return buildReferralProgramRulesRevShareLimit( - totalAwardPoolValue, - minQualifiedRevenueContribution, - 0.5, // qualifiedRevenueShare + awardPool, + minBaseRevenueContribution, + baseAnnualRevenueContribution, + 0.5, // maxBaseRevenueShare parseTimestamp("2026-01-01T00:00:00Z"), parseTimestamp("2026-12-31T23:59:59Z"), { chainId: 1, address: "0x57f1887a8bf19b14fc0df6fd9b2acc9af147ea85" }, @@ -87,8 +91,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 ──────────────────────────────────────────────────────────────────── @@ -109,12 +113,12 @@ describe("buildReferrerLeaderboardRevShareLimit", () => { grandTotalReferrals: 0, grandTotalIncrementalDuration: 0, grandTotalRevenueContribution: ZERO_ETH, - awardPoolRemaining: rules.totalAwardPoolValue, + awardPoolRemaining: rules.awardPool, }); }); describe("Scenario A — unqualified referrer: no award claimed", () => { - it("accumulates standard award but awardPoolApproxValue 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(); @@ -124,22 +128,20 @@ 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); + // 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.totalAwardPoolValue.amount, - ); + expect(result.aggregatedMetrics.awardPoolRemaining.amount).toBe(rules.awardPool.amount); }); }); - 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)), @@ -150,14 +152,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.uncappedAward.amount).toBe(UNCAPPED_AWARD_1Y.amount); // Claims all accumulated: 2 × $1.25 = $2.50 - expect(referrer.awardPoolApproxValue.amount).toBe(STANDARD_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("awardPoolApproxValue 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); @@ -170,19 +172,19 @@ 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); + // 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); }); }); - 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 = [ @@ -194,15 +196,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); + // 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("awardPoolApproxValue 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 @@ -216,10 +218,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); + // 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); }); @@ -235,8 +237,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.uncappedAward.amount).toBe(UNCAPPED_AWARD_1Y.amount); + expect(referrer.cappedAward.amount).toBe(0n); }); }); @@ -256,16 +258,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.cappedAward.amount).toBe(UNCAPPED_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.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 awardPoolApproxValue", () => { + 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 @@ -279,8 +281,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.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); }); @@ -301,13 +303,13 @@ describe("buildReferrerLeaderboardRevShareLimit", () => { const referrerB = result.referrers.get(ADDR_B)!; const referrerC = result.referrers.get(ADDR_C)!; - // Non-truncated: full standard award - expect(referrerA.awardPoolApproxValue.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); + // Non-truncated: full uncapped award + expect(referrerA.cappedAward.amount).toBe(UNCAPPED_AWARD_1Y.amount); + // Partially truncated: less than uncapped but > 0 + expect(referrerB.cappedAward.amount).toBeGreaterThan(0n); + expect(referrerB.cappedAward.amount).toBeLessThan(UNCAPPED_AWARD_1Y.amount); // Fully truncated: pool empty - expect(referrerC.awardPoolApproxValue.amount).toBe(0n); + expect(referrerC.cappedAward.amount).toBe(0n); expect(result.aggregatedMetrics.awardPoolRemaining.amount).toBe(0n); }); }); @@ -329,19 +331,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)!.awardPoolApproxValue.amount).toBe( - STANDARD_AWARD_1Y.amount, - ); - expect(result.referrers.get(ADDR_B)!.awardPoolApproxValue.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 qualifiedAwardValue desc, then standardAwardValue desc", () => { + it("ranks referrers by cappedAward desc, then uncappedAward 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), @@ -351,18 +351,18 @@ 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); }); - it("two fully-truncated referrers are ranked by standardAwardValue 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 + 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)); const events = [ makeEvent(ADDR_A, 1000, SECONDS_PER_YEAR * 2), @@ -371,7 +371,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 (longer duration) → rank 1 expect(result.referrers.get(ADDR_A)!.rank).toBe(1); expect(result.referrers.get(ADDR_B)!.rank).toBe(2); }); @@ -407,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.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.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); + }); + }); + describe("Aggregated metrics", () => { it("correctly sums grandTotalReferrals and grandTotalIncrementalDuration", () => { const rules = buildTestRules(); @@ -438,14 +476,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.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: awardPoolApproxValue = 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"), [ @@ -463,12 +501,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.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.awardPoolApproxValue.amount).toBe(STANDARD_AWARD_1Y.amount); + expect(referrerB.cappedAward.amount).toBe(UNCAPPED_AWARD_1Y.amount); }); it("disqualified referrer who never met the revenue threshold: pool unchanged", () => { @@ -484,15 +522,15 @@ 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.cappedAward.amount).toBe(0n); // Pool fully intact expect(result.aggregatedMetrics.awardPoolRemaining.amount).toBe(parseUsdc("1000").amount); }); 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) @@ -515,18 +553,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.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.awardPoolApproxValue.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.awardPoolApproxValue.amount).toBe(0n); + expect(referrerC.cappedAward.amount).toBe(0n); }); it("multiple disqualifications: all disqualified referrers get isAdminDisqualified=true", () => { @@ -548,17 +586,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.cappedAward.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.cappedAward.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.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 41c79cd1f1..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 @@ -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; } /** @@ -88,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). @@ -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.qualifiedRevenueShare, + 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.qualifiedRevenueShare, + 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 (awardPoolApproxValue) 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. @@ -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: cappedAward 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, @@ -221,15 +213,15 @@ export const buildReferrerLeaderboardRevShareLimit = ( rules, ); - const standardAwardValue = scalePrice( + const uncappedAward = scalePrice( revShareMetrics.totalBaseRevenueContribution, - rules.qualifiedRevenueShare, + rules.maxBaseRevenueShare, ); return buildAwardedReferrerMetricsRevShareLimit( rankedMetrics, - standardAwardValue, - priceUsdc(state.qualifiedAwardValueAmount), + 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 b5d0d65719..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 @@ -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; }; @@ -70,10 +69,14 @@ 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 is eligible for an award under the {@link ReferralProgramRulesRevShareLimit}. + * + * 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. * * @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; @@ -98,7 +101,7 @@ export const validateRankedReferrerMetricsRevShareLimit = ( metrics: RankedReferrerMetricsRevShareLimit, rules: ReferralProgramRulesRevShareLimit, ): void => { - validateReferrerMetricsRevShareLimit(metrics); + validateReferrerMetricsRevShareLimit(metrics, rules); validateReferrerRank(metrics.rank); const expectedIsQualified = isReferrerQualifiedRevShareLimit( @@ -154,29 +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 standard (uncapped) USDC award value for this referrer, computed as - * `qualifiedRevenueShare × totalBaseRevenueContribution`. + * 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 and qualification status. + * Independent of the pool state, qualification status, and admin disqualification status. */ - standardAwardValue: PriceUsdc; + uncappedAward: PriceUsdc; /** - * The approximate USDC value of the referrer's 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 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 Always <= standardAwardValue.amount + * @invariant Guaranteed to be a valid PriceUsdc with amount between 0 and {@link ReferralProgramRulesRevShareLimit.awardPool.amount} (inclusive) + * @invariant Always <= uncappedAward.amount * @invariant Amount equal to 0 when {@link isAdminDisqualified} is true. + * @invariant Amount equal to 0 when {@link isQualified} is false. */ - awardPoolApproxValue: PriceUsdc; + cappedAward: PriceUsdc; } export const validateAwardedReferrerMetricsRevShareLimit = ( @@ -185,43 +189,47 @@ export const validateAwardedReferrerMetricsRevShareLimit = ( ): void => { validateRankedReferrerMetricsRevShareLimit(metrics, rules); - makePriceUsdcSchema("AwardedReferrerMetricsRevShareLimit.standardAwardValue").parse( - metrics.standardAwardValue, + makePriceUsdcSchema("AwardedReferrerMetricsRevShareLimit.uncappedAward").parse( + metrics.uncappedAward, ); - makePriceUsdcSchema("AwardedReferrerMetricsRevShareLimit.awardPoolApproxValue").parse( - metrics.awardPoolApproxValue, - ); + makePriceUsdcSchema("AwardedReferrerMetricsRevShareLimit.cappedAward").parse(metrics.cappedAward); + + if (metrics.isAdminDisqualified && metrics.cappedAward.amount !== 0n) { + throw new Error( + `AwardedReferrerMetricsRevShareLimit: cappedAward.amount must be 0n for admin-disqualified referrers, got ${metrics.cappedAward.amount.toString()}.`, + ); + } - if (metrics.isAdminDisqualified && metrics.awardPoolApproxValue.amount !== 0n) { + if (!metrics.isQualified && metrics.cappedAward.amount !== 0n) { throw new Error( - `AwardedReferrerMetricsRevShareLimit: awardPoolApproxValue.amount must be 0n for admin-disqualified referrers, got ${metrics.awardPoolApproxValue.amount.toString()}.`, + `AwardedReferrerMetricsRevShareLimit: cappedAward.amount must be 0n for unqualified referrers, got ${metrics.cappedAward.amount.toString()}.`, ); } - if (metrics.awardPoolApproxValue.amount > rules.totalAwardPoolValue.amount) { + if (metrics.cappedAward.amount > rules.awardPool.amount) { throw new Error( - `AwardedReferrerMetricsRevShareLimit: awardPoolApproxValue.amount ${metrics.awardPoolApproxValue.amount.toString()} exceeds totalAwardPoolValue.amount ${rules.totalAwardPoolValue.amount.toString()}.`, + `AwardedReferrerMetricsRevShareLimit: cappedAward.amount ${metrics.cappedAward.amount.toString()} exceeds awardPool.amount ${rules.awardPool.amount.toString()}.`, ); } - if (metrics.awardPoolApproxValue.amount > metrics.standardAwardValue.amount) { + if (metrics.cappedAward.amount > metrics.uncappedAward.amount) { throw new Error( - `AwardedReferrerMetricsRevShareLimit: awardPoolApproxValue.amount ${metrics.awardPoolApproxValue.amount.toString()} exceeds standardAwardValue.amount ${metrics.standardAwardValue.amount.toString()}.`, + `AwardedReferrerMetricsRevShareLimit: cappedAward.amount ${metrics.cappedAward.amount.toString()} exceeds uncappedAward.amount ${metrics.uncappedAward.amount.toString()}.`, ); } }; export const buildAwardedReferrerMetricsRevShareLimit = ( referrer: RankedReferrerMetricsRevShareLimit, - standardAwardValue: PriceUsdc, - awardPoolApproxValue: PriceUsdc, + uncappedAward: PriceUsdc, + cappedAward: PriceUsdc, rules: ReferralProgramRulesRevShareLimit, ): AwardedReferrerMetricsRevShareLimit => { const result = { ...referrer, - standardAwardValue, - awardPoolApproxValue, + uncappedAward, + cappedAward, } satisfies AwardedReferrerMetricsRevShareLimit; validateAwardedReferrerMetricsRevShareLimit(result, rules); @@ -307,21 +315,21 @@ export const validateUnrankedReferrerMetricsRevShareLimit = ( ); } - makePriceUsdcSchema("UnrankedReferrerMetricsRevShareLimit.standardAwardValue").parse( - metrics.standardAwardValue, + makePriceUsdcSchema("UnrankedReferrerMetricsRevShareLimit.uncappedAward").parse( + metrics.uncappedAward, ); - if (metrics.standardAwardValue.amount !== 0n) { + if (metrics.uncappedAward.amount !== 0n) { throw new Error( - `Invalid UnrankedReferrerMetricsRevShareLimit: standardAwardValue.amount must be 0n, got: ${metrics.standardAwardValue.amount.toString()}.`, + `Invalid UnrankedReferrerMetricsRevShareLimit: uncappedAward.amount must be 0n, got: ${metrics.uncappedAward.amount.toString()}.`, ); } - makePriceUsdcSchema("UnrankedReferrerMetricsRevShareLimit.awardPoolApproxValue").parse( - metrics.awardPoolApproxValue, + makePriceUsdcSchema("UnrankedReferrerMetricsRevShareLimit.cappedAward").parse( + metrics.cappedAward, ); - if (metrics.awardPoolApproxValue.amount !== 0n) { + if (metrics.cappedAward.amount !== 0n) { throw new Error( - `Invalid UnrankedReferrerMetricsRevShareLimit: awardPoolApproxValue.amount must be 0n, got: ${metrics.awardPoolApproxValue.amount.toString()}.`, + `Invalid UnrankedReferrerMetricsRevShareLimit: cappedAward.amount must be 0n, got: ${metrics.cappedAward.amount.toString()}.`, ); } }; @@ -343,8 +351,8 @@ export const buildUnrankedReferrerMetricsRevShareLimit = ( totalBaseRevenueContribution: priceUsdc(0n), rank: null, isQualified: false, - standardAwardValue: priceUsdc(0n), - awardPoolApproxValue: priceUsdc(0n), + uncappedAward: priceUsdc(0n), + cappedAward: 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..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,40 +29,41 @@ 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. */ - minQualifiedRevenueContribution: PriceUsdc; + 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) */ - qualifiedRevenueShare: number; + maxBaseRevenueShare: number; /** * Admin-imposed disqualifications for this edition. @@ -81,21 +77,23 @@ 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.minQualifiedRevenueContribution").parse( - rules.minQualifiedRevenueContribution, + makePriceUsdcSchema("ReferralProgramRulesRevShareLimit.baseAnnualRevenueContribution").parse( + rules.baseAnnualRevenueContribution, ); 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}.`, ); } @@ -120,9 +118,10 @@ export const validateReferralProgramRulesRevShareLimit = ( }; export const buildReferralProgramRulesRevShareLimit = ( - totalAwardPoolValue: PriceUsdc, - minQualifiedRevenueContribution: PriceUsdc, - qualifiedRevenueShare: number, + awardPool: PriceUsdc, + minBaseRevenueContribution: PriceUsdc, + baseAnnualRevenueContribution: PriceUsdc, + maxBaseRevenueShare: number, startTime: UnixTimestamp, endTime: UnixTimestamp, subregistryId: AccountId, @@ -132,9 +131,10 @@ export const buildReferralProgramRulesRevShareLimit = ( ): ReferralProgramRulesRevShareLimit => { const result = { awardModel: ReferralProgramAwardModels.RevShareLimit, - totalAwardPoolValue, - minQualifiedRevenueContribution, - qualifiedRevenueShare, + awardPool, + minBaseRevenueContribution, + baseAnnualRevenueContribution, + 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 ); } 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,