diff --git a/indexer/packages/v4-protos/src/codegen/dydxprotocol/affiliates/affiliates.ts b/indexer/packages/v4-protos/src/codegen/dydxprotocol/affiliates/affiliates.ts index 6c3e30c866..23ea834128 100644 --- a/indexer/packages/v4-protos/src/codegen/dydxprotocol/affiliates/affiliates.ts +++ b/indexer/packages/v4-protos/src/codegen/dydxprotocol/affiliates/affiliates.ts @@ -17,7 +17,12 @@ export interface AffiliateTiersSDKType { export interface AffiliateTiers_Tier { /** Required all-time referred volume in quote quantums. */ reqReferredVolumeQuoteQuantums: Long; - /** Required currently staked native tokens (in whole coins). */ + /** + * Required currently staked native tokens (in whole coins). + * This is deprecated + */ + + /** @deprecated */ reqStakedWholeCoins: number; /** Taker fee share in parts-per-million. */ @@ -29,7 +34,12 @@ export interface AffiliateTiers_Tier { export interface AffiliateTiers_TierSDKType { /** Required all-time referred volume in quote quantums. */ req_referred_volume_quote_quantums: Long; - /** Required currently staked native tokens (in whole coins). */ + /** + * Required currently staked native tokens (in whole coins). + * This is deprecated + */ + + /** @deprecated */ req_staked_whole_coins: number; /** Taker fee share in parts-per-million. */ diff --git a/indexer/packages/v4-protos/src/codegen/dydxprotocol/affiliates/query.ts b/indexer/packages/v4-protos/src/codegen/dydxprotocol/affiliates/query.ts index 554822559d..3525a0d648 100644 --- a/indexer/packages/v4-protos/src/codegen/dydxprotocol/affiliates/query.ts +++ b/indexer/packages/v4-protos/src/codegen/dydxprotocol/affiliates/query.ts @@ -48,6 +48,9 @@ export interface AffiliateInfoResponse { /** The affiliate's 30d referred volume in quote quantums. */ referredVolume_30dRolling: Uint8Array; + /** The affiliate's 30d attributed volume in quote quantums (from referees). */ + + attributedVolume_30dRolling: Uint8Array; } /** * AffiliateInfoResponse is the response type for the Query/AffiliateInfo RPC @@ -80,6 +83,9 @@ export interface AffiliateInfoResponseSDKType { /** The affiliate's 30d referred volume in quote quantums. */ referred_volume_30d_rolling: Uint8Array; + /** The affiliate's 30d attributed volume in quote quantums (from referees). */ + + attributed_volume_30d_rolling: Uint8Array; } /** ReferredByRequest is the request type for the Query/ReferredBy RPC method. */ @@ -272,7 +278,8 @@ function createBaseAffiliateInfoResponse(): AffiliateInfoResponse { feeSharePpm: 0, referredVolume: new Uint8Array(), stakedAmount: new Uint8Array(), - referredVolume_30dRolling: new Uint8Array() + referredVolume_30dRolling: new Uint8Array(), + attributedVolume_30dRolling: new Uint8Array() }; } @@ -302,6 +309,10 @@ export const AffiliateInfoResponse = { writer.uint32(50).bytes(message.referredVolume_30dRolling); } + if (message.attributedVolume_30dRolling.length !== 0) { + writer.uint32(58).bytes(message.attributedVolume_30dRolling); + } + return writer; }, @@ -338,6 +349,10 @@ export const AffiliateInfoResponse = { message.referredVolume_30dRolling = reader.bytes(); break; + case 7: + message.attributedVolume_30dRolling = reader.bytes(); + break; + default: reader.skipType(tag & 7); break; @@ -355,6 +370,7 @@ export const AffiliateInfoResponse = { message.referredVolume = object.referredVolume ?? new Uint8Array(); message.stakedAmount = object.stakedAmount ?? new Uint8Array(); message.referredVolume_30dRolling = object.referredVolume_30dRolling ?? new Uint8Array(); + message.attributedVolume_30dRolling = object.attributedVolume_30dRolling ?? new Uint8Array(); return message; } diff --git a/indexer/packages/v4-protos/src/codegen/dydxprotocol/stats/stats.ts b/indexer/packages/v4-protos/src/codegen/dydxprotocol/stats/stats.ts index 63e5a2d4f6..d8bfa0751a 100644 --- a/indexer/packages/v4-protos/src/codegen/dydxprotocol/stats/stats.ts +++ b/indexer/packages/v4-protos/src/codegen/dydxprotocol/stats/stats.ts @@ -1,6 +1,82 @@ import { Timestamp } from "../../google/protobuf/timestamp"; import * as _m0 from "protobufjs/minimal"; -import { DeepPartial, Long, toTimestamp, fromTimestamp } from "../../helpers"; +import { Long, DeepPartial, toTimestamp, fromTimestamp } from "../../helpers"; +/** Role indicates whether this attribution is for the taker or maker */ + +export enum AffiliateAttribution_Role { + ROLE_UNSPECIFIED = 0, + ROLE_TAKER = 1, + ROLE_MAKER = 2, + UNRECOGNIZED = -1, +} +/** Role indicates whether this attribution is for the taker or maker */ + +export enum AffiliateAttribution_RoleSDKType { + ROLE_UNSPECIFIED = 0, + ROLE_TAKER = 1, + ROLE_MAKER = 2, + UNRECOGNIZED = -1, +} +export function affiliateAttribution_RoleFromJSON(object: any): AffiliateAttribution_Role { + switch (object) { + case 0: + case "ROLE_UNSPECIFIED": + return AffiliateAttribution_Role.ROLE_UNSPECIFIED; + + case 1: + case "ROLE_TAKER": + return AffiliateAttribution_Role.ROLE_TAKER; + + case 2: + case "ROLE_MAKER": + return AffiliateAttribution_Role.ROLE_MAKER; + + case -1: + case "UNRECOGNIZED": + default: + return AffiliateAttribution_Role.UNRECOGNIZED; + } +} +export function affiliateAttribution_RoleToJSON(object: AffiliateAttribution_Role): string { + switch (object) { + case AffiliateAttribution_Role.ROLE_UNSPECIFIED: + return "ROLE_UNSPECIFIED"; + + case AffiliateAttribution_Role.ROLE_TAKER: + return "ROLE_TAKER"; + + case AffiliateAttribution_Role.ROLE_MAKER: + return "ROLE_MAKER"; + + case AffiliateAttribution_Role.UNRECOGNIZED: + default: + return "UNRECOGNIZED"; + } +} +/** AffiliateAttribution represents the affiliate attribution for a fill. */ + +export interface AffiliateAttribution { + /** Role of the trader (taker or maker) whose affiliate is being attributed */ + role: AffiliateAttribution_Role; + /** Referrer address (the affiliate receiving the fee) */ + + referrerAddress: string; + /** Referred volume in quote quantums (capped based on 30-day volume limits) */ + + referredVolumeQuoteQuantums: Long; +} +/** AffiliateAttribution represents the affiliate attribution for a fill. */ + +export interface AffiliateAttributionSDKType { + /** Role of the trader (taker or maker) whose affiliate is being attributed */ + role: AffiliateAttribution_RoleSDKType; + /** Referrer address (the affiliate receiving the fee) */ + + referrer_address: string; + /** Referred volume in quote quantums (capped based on 30-day volume limits) */ + + referred_volume_quote_quantums: Long; +} /** BlockStats is used to store stats transiently within the scope of a block. */ export interface BlockStats { @@ -34,6 +110,12 @@ export interface BlockStats_Fill { */ affiliateFeeGeneratedQuantums: Long; + /** + * Affiliate revenue attributions for this fill (can include both taker and + * maker) + */ + + affiliateAttributions: AffiliateAttribution[]; } /** Fill records data about a fill on this block. */ @@ -56,6 +138,12 @@ export interface BlockStats_FillSDKType { */ affiliate_fee_generated_quantums: Long; + /** + * Affiliate revenue attributions for this fill (can include both taker and + * maker) + */ + + affiliate_attributions: AffiliateAttributionSDKType[]; } /** StatsMetadata stores metadata for the x/stats module */ @@ -134,6 +222,12 @@ export interface UserStats { /** Referred volume in quote quantums with this user being an affiliate */ affiliate_30dReferredVolumeQuoteQuantums: Long; + /** + * Attributed volume in quote quantums - volume from this user (as referee) + * that has been attributed to their affiliate in the last 30 days + */ + + affiliate_30dAttributedVolumeQuoteQuantums: Long; } /** * UserStats stores stats for a User. This is the sum of all stats for a user in @@ -152,6 +246,12 @@ export interface UserStatsSDKType { /** Referred volume in quote quantums with this user being an affiliate */ affiliate_30d_referred_volume_quote_quantums: Long; + /** + * Attributed volume in quote quantums - volume from this user (as referee) + * that has been attributed to their affiliate in the last 30 days + */ + + affiliate_30d_attributed_volume_quote_quantums: Long; } /** CachedStakedBaseTokens stores the last calculated total staked base tokens */ @@ -178,6 +278,71 @@ export interface CachedStakedBaseTokensSDKType { cached_at: Long; } +function createBaseAffiliateAttribution(): AffiliateAttribution { + return { + role: 0, + referrerAddress: "", + referredVolumeQuoteQuantums: Long.UZERO + }; +} + +export const AffiliateAttribution = { + encode(message: AffiliateAttribution, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.role !== 0) { + writer.uint32(8).int32(message.role); + } + + if (message.referrerAddress !== "") { + writer.uint32(18).string(message.referrerAddress); + } + + if (!message.referredVolumeQuoteQuantums.isZero()) { + writer.uint32(24).uint64(message.referredVolumeQuoteQuantums); + } + + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): AffiliateAttribution { + const reader = input instanceof _m0.Reader ? input : new _m0.Reader(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseAffiliateAttribution(); + + while (reader.pos < end) { + const tag = reader.uint32(); + + switch (tag >>> 3) { + case 1: + message.role = (reader.int32() as any); + break; + + case 2: + message.referrerAddress = reader.string(); + break; + + case 3: + message.referredVolumeQuoteQuantums = (reader.uint64() as Long); + break; + + default: + reader.skipType(tag & 7); + break; + } + } + + return message; + }, + + fromPartial(object: DeepPartial): AffiliateAttribution { + const message = createBaseAffiliateAttribution(); + message.role = object.role ?? 0; + message.referrerAddress = object.referrerAddress ?? ""; + message.referredVolumeQuoteQuantums = object.referredVolumeQuoteQuantums !== undefined && object.referredVolumeQuoteQuantums !== null ? Long.fromValue(object.referredVolumeQuoteQuantums) : Long.UZERO; + return message; + } + +}; + function createBaseBlockStats(): BlockStats { return { fills: [] @@ -228,7 +393,8 @@ function createBaseBlockStats_Fill(): BlockStats_Fill { taker: "", maker: "", notional: Long.UZERO, - affiliateFeeGeneratedQuantums: Long.UZERO + affiliateFeeGeneratedQuantums: Long.UZERO, + affiliateAttributions: [] }; } @@ -250,6 +416,10 @@ export const BlockStats_Fill = { writer.uint32(32).uint64(message.affiliateFeeGeneratedQuantums); } + for (const v of message.affiliateAttributions) { + AffiliateAttribution.encode(v!, writer.uint32(42).fork()).ldelim(); + } + return writer; }, @@ -278,6 +448,10 @@ export const BlockStats_Fill = { message.affiliateFeeGeneratedQuantums = (reader.uint64() as Long); break; + case 5: + message.affiliateAttributions.push(AffiliateAttribution.decode(reader, reader.uint32())); + break; + default: reader.skipType(tag & 7); break; @@ -293,6 +467,7 @@ export const BlockStats_Fill = { message.maker = object.maker ?? ""; message.notional = object.notional !== undefined && object.notional !== null ? Long.fromValue(object.notional) : Long.UZERO; message.affiliateFeeGeneratedQuantums = object.affiliateFeeGeneratedQuantums !== undefined && object.affiliateFeeGeneratedQuantums !== null ? Long.fromValue(object.affiliateFeeGeneratedQuantums) : Long.UZERO; + message.affiliateAttributions = object.affiliateAttributions?.map(e => AffiliateAttribution.fromPartial(e)) || []; return message; } @@ -503,7 +678,8 @@ function createBaseUserStats(): UserStats { takerNotional: Long.UZERO, makerNotional: Long.UZERO, affiliate_30dRevenueGeneratedQuantums: Long.UZERO, - affiliate_30dReferredVolumeQuoteQuantums: Long.UZERO + affiliate_30dReferredVolumeQuoteQuantums: Long.UZERO, + affiliate_30dAttributedVolumeQuoteQuantums: Long.UZERO }; } @@ -525,6 +701,10 @@ export const UserStats = { writer.uint32(32).uint64(message.affiliate_30dReferredVolumeQuoteQuantums); } + if (!message.affiliate_30dAttributedVolumeQuoteQuantums.isZero()) { + writer.uint32(40).uint64(message.affiliate_30dAttributedVolumeQuoteQuantums); + } + return writer; }, @@ -553,6 +733,10 @@ export const UserStats = { message.affiliate_30dReferredVolumeQuoteQuantums = (reader.uint64() as Long); break; + case 5: + message.affiliate_30dAttributedVolumeQuoteQuantums = (reader.uint64() as Long); + break; + default: reader.skipType(tag & 7); break; @@ -568,6 +752,7 @@ export const UserStats = { message.makerNotional = object.makerNotional !== undefined && object.makerNotional !== null ? Long.fromValue(object.makerNotional) : Long.UZERO; message.affiliate_30dRevenueGeneratedQuantums = object.affiliate_30dRevenueGeneratedQuantums !== undefined && object.affiliate_30dRevenueGeneratedQuantums !== null ? Long.fromValue(object.affiliate_30dRevenueGeneratedQuantums) : Long.UZERO; message.affiliate_30dReferredVolumeQuoteQuantums = object.affiliate_30dReferredVolumeQuoteQuantums !== undefined && object.affiliate_30dReferredVolumeQuoteQuantums !== null ? Long.fromValue(object.affiliate_30dReferredVolumeQuoteQuantums) : Long.UZERO; + message.affiliate_30dAttributedVolumeQuoteQuantums = object.affiliate_30dAttributedVolumeQuoteQuantums !== undefined && object.affiliate_30dAttributedVolumeQuoteQuantums !== null ? Long.fromValue(object.affiliate_30dAttributedVolumeQuoteQuantums) : Long.UZERO; return message; } diff --git a/proto/dydxprotocol/affiliates/affiliates.proto b/proto/dydxprotocol/affiliates/affiliates.proto index ecc1fb3658..a6badf8805 100644 --- a/proto/dydxprotocol/affiliates/affiliates.proto +++ b/proto/dydxprotocol/affiliates/affiliates.proto @@ -13,7 +13,8 @@ message AffiliateTiers { // Required all-time referred volume in quote quantums. uint64 req_referred_volume_quote_quantums = 1; // Required currently staked native tokens (in whole coins). - uint32 req_staked_whole_coins = 2; + // This is deprecated + uint32 req_staked_whole_coins = 2 [ deprecated = true ]; // Taker fee share in parts-per-million. uint32 taker_fee_share_ppm = 3; } diff --git a/proto/dydxprotocol/affiliates/query.proto b/proto/dydxprotocol/affiliates/query.proto index 6d6697ef97..0bd060ff20 100644 --- a/proto/dydxprotocol/affiliates/query.proto +++ b/proto/dydxprotocol/affiliates/query.proto @@ -82,6 +82,12 @@ message AffiliateInfoResponse { "github.com/dydxprotocol/v4-chain/protocol/dtypes.SerializableInt", (gogoproto.nullable) = false ]; + // The affiliate's 30d attributed volume in quote quantums (from referees). + bytes attributed_volume_30d_rolling = 7 [ + (gogoproto.customtype) = + "github.com/dydxprotocol/v4-chain/protocol/dtypes.SerializableInt", + (gogoproto.nullable) = false + ]; } // ReferredByRequest is the request type for the Query/ReferredBy RPC method. diff --git a/proto/dydxprotocol/stats/stats.proto b/proto/dydxprotocol/stats/stats.proto index ca1841ac9f..ce755f07c7 100644 --- a/proto/dydxprotocol/stats/stats.proto +++ b/proto/dydxprotocol/stats/stats.proto @@ -6,6 +6,23 @@ option go_package = "github.com/dydxprotocol/v4-chain/protocol/x/stats/types"; import "gogoproto/gogo.proto"; import "google/protobuf/timestamp.proto"; +// AffiliateAttribution represents the affiliate attribution for a fill. +message AffiliateAttribution { + // Role indicates whether this attribution is for the taker or maker + enum Role { + ROLE_UNSPECIFIED = 0; + ROLE_TAKER = 1; + ROLE_MAKER = 2; + } + + // Role of the trader (taker or maker) whose affiliate is being attributed + Role role = 1; + // Referrer address (the affiliate receiving the fee) + string referrer_address = 2; + // Referred volume in quote quantums (capped based on 30-day volume limits) + uint64 referred_volume_quote_quantums = 3; +} + // BlockStats is used to store stats transiently within the scope of a block. message BlockStats { // Fill records data about a fill on this block. @@ -24,6 +41,10 @@ message BlockStats { // Used to calculate affiliate revenue attributed for taker. This is dynamic // per affiliate tier uint64 affiliate_fee_generated_quantums = 4; + + // Affiliate revenue attributions for this fill (can include both taker and + // maker) + repeated AffiliateAttribution affiliate_attributions = 5; } // The fills that occured on this block. @@ -73,6 +94,10 @@ message UserStats { // Referred volume in quote quantums with this user being an affiliate uint64 affiliate_30d_referred_volume_quote_quantums = 4; + + // Attributed volume in quote quantums - volume from this user (as referee) + // that has been attributed to their affiliate in the last 30 days + uint64 affiliate_30d_attributed_volume_quote_quantums = 5; } // CachedStakedBaseTokens stores the last calculated total staked base tokens diff --git a/protocol/x/affiliates/abci.go b/protocol/x/affiliates/abci.go index 90063b492f..6199e68223 100644 --- a/protocol/x/affiliates/abci.go +++ b/protocol/x/affiliates/abci.go @@ -2,7 +2,6 @@ package affiliates import ( sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/dydxprotocol/v4-chain/protocol/lib/log" "github.com/dydxprotocol/v4-chain/protocol/x/affiliates/keeper" ) @@ -10,7 +9,4 @@ func EndBlocker( ctx sdk.Context, keeper *keeper.Keeper, ) { - if err := keeper.AggregateAffiliateReferredVolumeForFills(ctx); err != nil { - log.ErrorLogWithError(ctx, "error aggregating affiliate volume for fills", err) - } } diff --git a/protocol/x/affiliates/keeper/grpc_query.go b/protocol/x/affiliates/keeper/grpc_query.go index 287e4d6ba0..3526aee137 100644 --- a/protocol/x/affiliates/keeper/grpc_query.go +++ b/protocol/x/affiliates/keeper/grpc_query.go @@ -40,14 +40,16 @@ func (k Keeper) AffiliateInfo(c context.Context, userStats := k.statsKeeper.GetUserStats(ctx, addr.String()) referredVolume := userStats.Affiliate_30DReferredVolumeQuoteQuantums + attributedVolume := userStats.Affiliate_30DAttributedVolumeQuoteQuantums stakedAmount := k.statsKeeper.GetStakedBaseTokens(ctx, req.GetAddress()) return &types.AffiliateInfoResponse{ - IsWhitelisted: isWhitelisted, - Tier: tierLevel, - FeeSharePpm: feeSharePpm, - StakedAmount: dtypes.NewIntFromBigInt(stakedAmount), - ReferredVolume_30DRolling: dtypes.NewIntFromBigInt(lib.BigU(referredVolume)), + IsWhitelisted: isWhitelisted, + Tier: tierLevel, + FeeSharePpm: feeSharePpm, + StakedAmount: dtypes.NewIntFromBigInt(stakedAmount), + ReferredVolume_30DRolling: dtypes.NewIntFromBigInt(lib.BigU(referredVolume)), + AttributedVolume_30DRolling: dtypes.NewIntFromBigInt(lib.BigU(attributedVolume)), }, nil } diff --git a/protocol/x/affiliates/keeper/grpc_query_test.go b/protocol/x/affiliates/keeper/grpc_query_test.go index c990b89837..ec21cc071c 100644 --- a/protocol/x/affiliates/keeper/grpc_query_test.go +++ b/protocol/x/affiliates/keeper/grpc_query_test.go @@ -10,6 +10,7 @@ import ( "github.com/dydxprotocol/v4-chain/protocol/dtypes" "github.com/dydxprotocol/v4-chain/protocol/x/affiliates/keeper" "github.com/dydxprotocol/v4-chain/protocol/x/affiliates/types" + statstypes "github.com/dydxprotocol/v4-chain/protocol/x/stats/types" "github.com/stretchr/testify/require" testapp "github.com/dydxprotocol/v4-chain/protocol/testutil/app" @@ -35,6 +36,7 @@ func TestAffiliateInfo(t *testing.T) { ReferredVolume_30DRolling: dtypes.NewIntFromUint64( types.DefaultAffiliateTiers.Tiers[0].ReqReferredVolumeQuoteQuantums, ), + AttributedVolume_30DRolling: dtypes.NewIntFromUint64(0), StakedAmount: dtypes.NewIntFromUint64( uint64(types.DefaultAffiliateTiers.Tiers[0].ReqStakedWholeCoins) * 1e18, ), @@ -69,6 +71,7 @@ func TestAffiliateInfo(t *testing.T) { ReferredVolume_30DRolling: dtypes.NewIntFromUint64( types.DefaultAffiliateTiers.Tiers[0].ReqReferredVolumeQuoteQuantums, ), + AttributedVolume_30DRolling: dtypes.NewIntFromUint64(0), StakedAmount: dtypes.NewIntFromUint64( uint64(types.DefaultAffiliateTiers.Tiers[0].ReqStakedWholeCoins) * 1e18, ), @@ -104,11 +107,12 @@ func TestAffiliateInfo(t *testing.T) { Address: constants.AliceAccAddress.String(), }, res: &types.AffiliateInfoResponse{ - IsWhitelisted: true, - Tier: 4, - FeeSharePpm: 250_000, - ReferredVolume_30DRolling: dtypes.NewIntFromUint64(0), - StakedAmount: dtypes.NewIntFromUint64(0), + IsWhitelisted: true, + Tier: 4, + FeeSharePpm: 250_000, + ReferredVolume_30DRolling: dtypes.NewIntFromUint64(0), + AttributedVolume_30DRolling: dtypes.NewIntFromUint64(0), + StakedAmount: dtypes.NewIntFromUint64(0), }, setup: func(ctx sdk.Context, k keeper.Keeper, tApp *testapp.TestApp) { err := k.RegisterAffiliate(ctx, constants.BobAccAddress.String(), constants.AliceAccAddress.String()) @@ -132,6 +136,40 @@ func TestAffiliateInfo(t *testing.T) { require.NoError(t, err) }, }, + "With Attributed Volume": { + req: &types.AffiliateInfoRequest{ + Address: constants.AliceAccAddress.String(), + }, + res: &types.AffiliateInfoResponse{ + IsWhitelisted: false, + Tier: 0, + FeeSharePpm: types.DefaultAffiliateTiers.Tiers[0].TakerFeeSharePpm, + ReferredVolume_30DRolling: dtypes.NewIntFromUint64(5_000_000), + AttributedVolume_30DRolling: dtypes.NewIntFromUint64(3_000_000), + StakedAmount: dtypes.NewIntFromUint64(0), + }, + setup: func(ctx sdk.Context, k keeper.Keeper, tApp *testapp.TestApp) { + err := k.RegisterAffiliate(ctx, constants.BobAccAddress.String(), constants.AliceAccAddress.String()) + require.NoError(t, err) + + // Set user stats with both referred and attributed volume + statsKeeper := tApp.App.StatsKeeper + statsKeeper.SetUserStats(ctx, constants.AliceAccAddress.String(), &statstypes.UserStats{ + Affiliate_30DReferredVolumeQuoteQuantums: 5_000_000, + Affiliate_30DAttributedVolumeQuoteQuantums: 3_000_000, + }) + + stakingKeeper := tApp.App.StakingKeeper + err = stakingKeeper.SetDelegation(ctx, + stakingtypes.NewDelegation(constants.AliceAccAddress.String(), + constants.AliceValAddress.String(), math.LegacyNewDecFromBigInt( + big.NewInt(0), + ), + ), + ) + require.NoError(t, err) + }, + }, } for name, tc := range testCases { diff --git a/protocol/x/affiliates/keeper/keeper.go b/protocol/x/affiliates/keeper/keeper.go index 0af1433c08..fbf1b2aafa 100644 --- a/protocol/x/affiliates/keeper/keeper.go +++ b/protocol/x/affiliates/keeper/keeper.go @@ -233,10 +233,9 @@ func (k Keeper) UpdateAffiliateTiers(ctx sdk.Context, affiliateTiers types.Affil tiers[i].TakerFeeSharePpm, types.AffiliatesRevSharePpmCap) } // Check if the tiers are strictly increasing. - if tiers[i].ReqReferredVolumeQuoteQuantums <= tiers[i-1].ReqReferredVolumeQuoteQuantums || - tiers[i].ReqStakedWholeCoins < tiers[i-1].ReqStakedWholeCoins { + if tiers[i].ReqReferredVolumeQuoteQuantums <= tiers[i-1].ReqReferredVolumeQuoteQuantums { return errorsmod.Wrapf(types.ErrInvalidAffiliateTiers, - "volume must be strictly increasing; staked coins must be non-decreasing") + "volume must be strictly increasing") } } store.Set([]byte(types.AffiliateTiersKey), affiliateTiersBytes) @@ -376,106 +375,3 @@ func (k Keeper) GetAffiliateOverridesMap(ctx sdk.Context) (map[string]bool, erro } return affiliateOverridesMap, nil } - -func (k Keeper) addReferredVolumeIfQualified( - ctx sdk.Context, - referee string, - referrer string, - volume uint64, - affiliateParams types.AffiliateParameters, - previouslyAttributedVolume map[string]uint64, -) error { - // Get the user stats from the referee - refereeUserStats := k.statsKeeper.GetUserStats(ctx, referee) - if refereeUserStats == nil { - return errorsmod.Wrapf(types.ErrUpdatingAffiliateReferredVolume, - "referee %s, refereeUserStats is nil", referee) - } - - previousVolume := (refereeUserStats.TakerNotional + refereeUserStats.MakerNotional + - previouslyAttributedVolume[referee]) - - // If parameter is 0 then no limit is applied - cap := affiliateParams.Maximum_30DAttributableVolumePerReferredUserQuoteQuantums - if cap != 0 { - if previousVolume >= cap { - volume = 0 - } else if previousVolume+volume > cap { - // Remainder of the volume to get them to the cap - volume = cap - previousVolume - } - } - previouslyAttributedVolume[referee] += volume - - // Add the volume to the referrer on their 30d rolling window - if volume > 0 { - affiliateUserStats := k.statsKeeper.GetUserStats(ctx, referrer) - if affiliateUserStats != nil { - affiliateUserStats.Affiliate_30DReferredVolumeQuoteQuantums += volume - } - k.statsKeeper.SetUserStats(ctx, referrer, affiliateUserStats) - } - return nil -} - -func (k Keeper) AggregateAffiliateReferredVolumeForFills( - ctx sdk.Context, -) error { - blockStats := k.statsKeeper.GetBlockStats(ctx) - affiliateParams, err := k.GetAffiliateParameters(ctx) - if err != nil { - return err - } - referredByCache := make(map[string]string) - - // Multiple fills within the same block can happen, so we want to keep track of those to properly attribute volume. - previouslyAttributedVolume := make(map[string]uint64) - - for _, fill := range blockStats.Fills { - // Process taker's referred volume - referredByAddrTaker, cached := referredByCache[fill.Taker] - if !cached { - var found bool - referredByAddrTaker, found = k.GetReferredBy(ctx, fill.Taker) - if found { - referredByCache[fill.Taker] = referredByAddrTaker - } - } - if referredByAddrTaker != "" { - // Add referred volume, this decides affiliate tier and is limited by the maximum volume on a 30d window - if err := k.addReferredVolumeIfQualified( - ctx, - fill.Taker, - referredByAddrTaker, - fill.Notional, - affiliateParams, - previouslyAttributedVolume, - ); err != nil { - return err - } - } - - // Process maker's referred volume - referredByAddrMaker, cached := referredByCache[fill.Maker] - if !cached { - var found bool - referredByAddrMaker, found = k.GetReferredBy(ctx, fill.Maker) - if found { - referredByCache[fill.Maker] = referredByAddrMaker - } - } - if referredByAddrMaker != "" { - if err := k.addReferredVolumeIfQualified( - ctx, - fill.Maker, - referredByAddrMaker, - fill.Notional, - affiliateParams, - previouslyAttributedVolume, - ); err != nil { - return err - } - } - } - return nil -} diff --git a/protocol/x/affiliates/keeper/keeper_test.go b/protocol/x/affiliates/keeper/keeper_test.go index 017c2dd930..e64c1d3eaa 100644 --- a/protocol/x/affiliates/keeper/keeper_test.go +++ b/protocol/x/affiliates/keeper/keeper_test.go @@ -16,7 +16,6 @@ import ( keepertest "github.com/dydxprotocol/v4-chain/protocol/testutil/keeper" "github.com/dydxprotocol/v4-chain/protocol/x/affiliates/keeper" "github.com/dydxprotocol/v4-chain/protocol/x/affiliates/types" - statskeeper "github.com/dydxprotocol/v4-chain/protocol/x/stats/keeper" statstypes "github.com/dydxprotocol/v4-chain/protocol/x/stats/types" "github.com/stretchr/testify/require" ) @@ -318,24 +317,38 @@ func TestUpdateAffiliateTiers(t *testing.T) { expectedError: types.ErrInvalidAffiliateTiers, }, { - name: "Invalid tiers - decreasing staking requirement", + name: "Taker fee share ppm greater than cap", affiliateTiers: types.AffiliateTiers{ Tiers: []types.AffiliateTiers_Tier{ - {ReqReferredVolumeQuoteQuantums: 1000, ReqStakedWholeCoins: 200, TakerFeeSharePpm: 100}, - {ReqReferredVolumeQuoteQuantums: 2000, ReqStakedWholeCoins: 100, TakerFeeSharePpm: 200}, + {ReqReferredVolumeQuoteQuantums: 1000, ReqStakedWholeCoins: 100, TakerFeeSharePpm: 100}, + {ReqReferredVolumeQuoteQuantums: 2000, ReqStakedWholeCoins: 200, TakerFeeSharePpm: 550_000}, // 55% }, }, - expectedError: types.ErrInvalidAffiliateTiers, + expectedError: types.ErrRevShareSafetyViolation, }, { - name: "Taker fee share ppm greater than cap", + name: "Valid tiers with req_staked_whole_coins set to 0 (deprecated field)", affiliateTiers: types.AffiliateTiers{ Tiers: []types.AffiliateTiers_Tier{ - {ReqReferredVolumeQuoteQuantums: 1000, ReqStakedWholeCoins: 100, TakerFeeSharePpm: 100}, - {ReqReferredVolumeQuoteQuantums: 2000, ReqStakedWholeCoins: 200, TakerFeeSharePpm: 550_000}, // 55% + {ReqReferredVolumeQuoteQuantums: 0, ReqStakedWholeCoins: 0, TakerFeeSharePpm: 50_000}, // 5% + {ReqReferredVolumeQuoteQuantums: 1_000_000, ReqStakedWholeCoins: 0, TakerFeeSharePpm: 100_000}, // 10% + {ReqReferredVolumeQuoteQuantums: 10_000_000, ReqStakedWholeCoins: 0, TakerFeeSharePpm: 150_000}, // 15% + {ReqReferredVolumeQuoteQuantums: 100_000_000, ReqStakedWholeCoins: 0, TakerFeeSharePpm: 200_000}, // 20% }, }, - expectedError: types.ErrRevShareSafetyViolation, + expectedError: nil, + }, + { + name: "Valid tiers with mixed req_staked_whole_coins values including 0", + affiliateTiers: types.AffiliateTiers{ + Tiers: []types.AffiliateTiers_Tier{ + {ReqReferredVolumeQuoteQuantums: 0, ReqStakedWholeCoins: 0, TakerFeeSharePpm: 50_000}, + {ReqReferredVolumeQuoteQuantums: 1_000_000, ReqStakedWholeCoins: 0, TakerFeeSharePpm: 100_000}, + {ReqReferredVolumeQuoteQuantums: 10_000_000, ReqStakedWholeCoins: 100, TakerFeeSharePpm: 150_000}, + {ReqReferredVolumeQuoteQuantums: 100_000_000, ReqStakedWholeCoins: 500, TakerFeeSharePpm: 200_000}, + }, + }, + expectedError: nil, }, } @@ -654,421 +667,6 @@ func TestGetTakerFeeShareViaWhitelist(t *testing.T) { } } -func TestAggregateAffiliateReferredVolumeForFills(t *testing.T) { - affiliate := constants.AliceAccAddress.String() - referee1 := constants.BobAccAddress.String() - referee2 := constants.DaveAccAddress.String() - maker := constants.CarlAccAddress.String() - testCases := []struct { - name string - referrals int - expectedVolume *big.Int - referreeAddressesToVerify []string - expectedCommissions []*big.Int - expectedReferrer []string - expectedAttributedVolume []uint64 - setup func(t *testing.T, ctx sdk.Context, k *keeper.Keeper, statsKeeper *statskeeper.Keeper) - }{ - { - name: "0 referrals", - expectedVolume: big.NewInt(0), - expectedReferrer: []string{}, - expectedAttributedVolume: []uint64{}, - setup: func(t *testing.T, ctx sdk.Context, k *keeper.Keeper, statsKeeper *statskeeper.Keeper) { - statsKeeper.SetBlockStats(ctx, &statstypes.BlockStats{ - Fills: []*statstypes.BlockStats_Fill{ - { - Taker: referee1, - Maker: maker, - Notional: 100_000_000_000, - }, - }, - }) - }, - }, - { - name: "1 referral", - referrals: 1, - expectedVolume: big.NewInt(100_000_000_000), - expectedReferrer: []string{ - affiliate, - }, - expectedAttributedVolume: []uint64{ - 200_000_000_000, - }, - setup: func(t *testing.T, ctx sdk.Context, k *keeper.Keeper, statsKeeper *statskeeper.Keeper) { - err := k.RegisterAffiliate(ctx, referee1, affiliate) - require.NoError(t, err) - - // They are close to the maximum of attributable volume so we should not add more than expected - statsKeeper.SetUserStats(ctx, affiliate, &statstypes.UserStats{ - TakerNotional: 0, - MakerNotional: 0, - Affiliate_30DRevenueGeneratedQuantums: 0, - Affiliate_30DReferredVolumeQuoteQuantums: 100_000_000_000, - }) - - statsKeeper.SetBlockStats(ctx, &statstypes.BlockStats{ - Fills: []*statstypes.BlockStats_Fill{ - { - Taker: referee1, - Maker: maker, - Notional: 100_000_000_000, - AffiliateFeeGeneratedQuantums: 1_000_000_000, - }, - }, - }) - }, - }, - { - name: "2 referrals, no limit", - referrals: 2, - expectedVolume: big.NewInt(300_000_000_000), - expectedReferrer: []string{ - affiliate, - }, - expectedAttributedVolume: []uint64{ - 300_000_000_000, - }, - setup: func(t *testing.T, ctx sdk.Context, k *keeper.Keeper, statsKeeper *statskeeper.Keeper) { - err := k.RegisterAffiliate(ctx, referee1, affiliate) - require.NoError(t, err) - err = k.RegisterAffiliate(ctx, referee2, affiliate) - require.NoError(t, err) - - // They are close to the maximum of attributable volume so we should not add more than expected - statsKeeper.SetUserStats(ctx, affiliate, &statstypes.UserStats{ - TakerNotional: 0, - MakerNotional: 0, - Affiliate_30DRevenueGeneratedQuantums: 0, - Affiliate_30DReferredVolumeQuoteQuantums: 0, - }) - - statsKeeper.SetBlockStats(ctx, &statstypes.BlockStats{ - Fills: []*statstypes.BlockStats_Fill{ - { - Taker: referee1, - Maker: maker, - Notional: 100_000_000_000, - AffiliateFeeGeneratedQuantums: 1_000_000_000, - }, - { - Taker: referee2, - Maker: maker, - Notional: 200_000_000_000, - AffiliateFeeGeneratedQuantums: 2_000_000_000, - }, - }, - }) - }, - }, - { - name: "2 referrals, maker also referred", - referrals: 2, - expectedVolume: big.NewInt(600_000_000_000), - expectedReferrer: []string{ - affiliate, - }, - expectedAttributedVolume: []uint64{ - 600_000_000_000, - }, - setup: func(t *testing.T, ctx sdk.Context, k *keeper.Keeper, statsKeeper *statskeeper.Keeper) { - err := k.RegisterAffiliate(ctx, referee1, affiliate) - require.NoError(t, err) - err = k.RegisterAffiliate(ctx, referee2, affiliate) - require.NoError(t, err) - err = k.RegisterAffiliate(ctx, maker, affiliate) - require.NoError(t, err) - statsKeeper.SetBlockStats(ctx, &statstypes.BlockStats{ - Fills: []*statstypes.BlockStats_Fill{ - { - Taker: referee1, - Maker: maker, - Notional: 100_000_000_000, - AffiliateFeeGeneratedQuantums: 1_000_000_000, - }, - { - Taker: referee2, - Maker: maker, - Notional: 200_000_000_000, - AffiliateFeeGeneratedQuantums: 3_000_000_000, - }, - }, - }) - err = k.UpdateAffiliateParameters(ctx, &types.MsgUpdateAffiliateParameters{ - Authority: constants.GovAuthority, - AffiliateParameters: types.AffiliateParameters{ - Maximum_30DAttributableVolumePerReferredUserQuoteQuantums: 800_000_000_000, - }, - }) - require.NoError(t, err) - }, - }, - { - name: "2 referrals, takers not referred, maker referred", - referrals: 2, - expectedVolume: big.NewInt(300_000_000_000), - expectedReferrer: []string{ - affiliate, - }, - expectedAttributedVolume: []uint64{ - 200_000_000_000, - }, - setup: func(t *testing.T, ctx sdk.Context, k *keeper.Keeper, statsKeeper *statskeeper.Keeper) { - err := k.RegisterAffiliate(ctx, maker, affiliate) - require.NoError(t, err) - - // They are close to the maximum of attributable volume so we should not add more than expected - statsKeeper.SetUserStats(ctx, affiliate, &statstypes.UserStats{ - TakerNotional: 0, - MakerNotional: 0, - Affiliate_30DRevenueGeneratedQuantums: 0, - Affiliate_30DReferredVolumeQuoteQuantums: 0, - }) - - statsKeeper.SetBlockStats(ctx, &statstypes.BlockStats{ - Fills: []*statstypes.BlockStats_Fill{ - { - Taker: referee1, - Maker: maker, - Notional: 100_000_000_000, - AffiliateFeeGeneratedQuantums: 1_000_000_000, - }, - { - Taker: referee2, - Maker: maker, - Notional: 100_000_000_000, - AffiliateFeeGeneratedQuantums: 2_000_000_000, - }, - }, - }) - err = k.UpdateAffiliateParameters(ctx, &types.MsgUpdateAffiliateParameters{ - Authority: constants.GovAuthority, - AffiliateParameters: types.AffiliateParameters{ - Maximum_30DAttributableVolumePerReferredUserQuoteQuantums: 300_000_000_000, - }, - }) - require.NoError(t, err) - }, - }, - { - name: "2 referrals, reached maximum attributable revenue", - referrals: 2, - expectedVolume: big.NewInt(300_000_000_000), - expectedReferrer: []string{ - affiliate, - }, - expectedAttributedVolume: []uint64{ - 80_000_000_000, - }, - setup: func(t *testing.T, ctx sdk.Context, k *keeper.Keeper, statsKeeper *statskeeper.Keeper) { - err := k.RegisterAffiliate(ctx, referee1, affiliate) - require.NoError(t, err) - err = k.RegisterAffiliate(ctx, referee2, affiliate) - require.NoError(t, err) - - // They are close to the maximum of attributable volume so we should not add more than expected - statsKeeper.SetUserStats(ctx, affiliate, &statstypes.UserStats{ - TakerNotional: 0, - MakerNotional: 0, - Affiliate_30DRevenueGeneratedQuantums: 0, - Affiliate_30DReferredVolumeQuoteQuantums: 0, - }) - - // Maximum volume was reached per affiliate, so we should not add any attributable volume - statsKeeper.SetUserStats(ctx, referee1, &statstypes.UserStats{ - TakerNotional: 150_000_000_000, - MakerNotional: 100_000_000_000, - }) - statsKeeper.SetUserStats(ctx, referee2, &statstypes.UserStats{ - TakerNotional: 150_000_000_000, - MakerNotional: 100_000_000_000, - }) - - statsKeeper.SetBlockStats(ctx, &statstypes.BlockStats{ - Fills: []*statstypes.BlockStats_Fill{ - { - Taker: referee1, - Maker: maker, - Notional: 100_000_000_000, - AffiliateFeeGeneratedQuantums: 1_000_000_000, - }, - { - Taker: referee2, - Maker: maker, - Notional: 200_000_000_000, - AffiliateFeeGeneratedQuantums: 2_000_000_000, - }, - }, - }) - err = k.UpdateAffiliateParameters(ctx, &types.MsgUpdateAffiliateParameters{ - Authority: constants.GovAuthority, - AffiliateParameters: types.AffiliateParameters{ - Maximum_30DAttributableVolumePerReferredUserQuoteQuantums: 290_000_000_000, - }, - }) - require.NoError(t, err) - }, - }, - { - name: "2 referrals, test limits of attributable revenue", - referrals: 2, - expectedVolume: big.NewInt(300_000_000_000), - expectedReferrer: []string{ - affiliate, - }, - expectedAttributedVolume: []uint64{ - 200_000_000_000, - }, - setup: func(t *testing.T, ctx sdk.Context, k *keeper.Keeper, statsKeeper *statskeeper.Keeper) { - err := k.RegisterAffiliate(ctx, referee1, affiliate) - require.NoError(t, err) - err = k.RegisterAffiliate(ctx, referee2, affiliate) - require.NoError(t, err) - - // They are close to the maximum of attributable volume so we should not add more than expected - statsKeeper.SetUserStats(ctx, affiliate, &statstypes.UserStats{ - TakerNotional: 0, - MakerNotional: 0, - Affiliate_30DRevenueGeneratedQuantums: 0, - Affiliate_30DReferredVolumeQuoteQuantums: 0, - }) - - // They are close to the maximum of attributable volume so we should not add more than expected - statsKeeper.SetUserStats(ctx, referee1, &statstypes.UserStats{ - TakerNotional: 50_000_000_000, - MakerNotional: 100_000_000_000, - Affiliate_30DRevenueGeneratedQuantums: 1_000_000_000, - }) - statsKeeper.SetUserStats(ctx, referee2, &statstypes.UserStats{ - TakerNotional: 50_000_000_000, - MakerNotional: 100_000_000_000, - Affiliate_30DRevenueGeneratedQuantums: 1_000_000_000, - }) - - statsKeeper.SetBlockStats(ctx, &statstypes.BlockStats{ - Fills: []*statstypes.BlockStats_Fill{ - { - Taker: referee1, - Maker: maker, - Notional: 100_000_000_000, - AffiliateFeeGeneratedQuantums: 1_000_000_000, - }, - { - Taker: referee2, - Maker: maker, - Notional: 200_000_000_000, - AffiliateFeeGeneratedQuantums: 2_000_000_000, - }, - }, - }) - err = k.UpdateAffiliateParameters(ctx, &types.MsgUpdateAffiliateParameters{ - Authority: constants.GovAuthority, - AffiliateParameters: types.AffiliateParameters{ - // Each affiliate can only generate 250_000_000_000 quantums of attributable revenue on a 30d window - Maximum_30DAttributableVolumePerReferredUserQuoteQuantums: 250_000_000_000, - }, - }) - require.NoError(t, err) - }, - }, - { - name: "maker is also affiliate, make sure attributed volume doesn't exceed max per user", - referrals: 2, - expectedVolume: big.NewInt(600_000_000_000), - expectedReferrer: []string{ - affiliate, - }, - expectedAttributedVolume: []uint64{ - 350_000_000_000, - }, - setup: func(t *testing.T, ctx sdk.Context, k *keeper.Keeper, statsKeeper *statskeeper.Keeper) { - err := k.RegisterAffiliate(ctx, referee1, affiliate) - require.NoError(t, err) - err = k.RegisterAffiliate(ctx, referee2, affiliate) - require.NoError(t, err) - err = k.RegisterAffiliate(ctx, maker, affiliate) - require.NoError(t, err) - - // They are close to the maximum of attributable volume so we should not add more than expected - statsKeeper.SetUserStats(ctx, affiliate, &statstypes.UserStats{ - TakerNotional: 0, - MakerNotional: 0, - Affiliate_30DRevenueGeneratedQuantums: 0, - Affiliate_30DReferredVolumeQuoteQuantums: 0, - }) - // Starts with 150M volume - statsKeeper.SetUserStats(ctx, referee1, &statstypes.UserStats{ - TakerNotional: 50_000_000_000, - MakerNotional: 100_000_000_000, - Affiliate_30DRevenueGeneratedQuantums: 1_000_000_000, - }) - // starts with 150M volume - statsKeeper.SetUserStats(ctx, referee2, &statstypes.UserStats{ - TakerNotional: 50_000_000_000, - MakerNotional: 100_000_000_000, - Affiliate_30DRevenueGeneratedQuantums: 1_000_000_000, - }) - // Starts with 100M volume - statsKeeper.SetUserStats(ctx, maker, &statstypes.UserStats{ - TakerNotional: 50_000_000_000, - MakerNotional: 50_000_000_000, - Affiliate_30DRevenueGeneratedQuantums: 1_000_000_000, - }) - - statsKeeper.SetBlockStats(ctx, &statstypes.BlockStats{ - Fills: []*statstypes.BlockStats_Fill{ - { - Taker: referee1, - Maker: maker, - Notional: 100_000_000_000, - AffiliateFeeGeneratedQuantums: 1_000_000_000, - }, - { - Taker: referee2, - Maker: maker, - Notional: 200_000_000_000, - AffiliateFeeGeneratedQuantums: 2_000_000_000, - }, - }, - }) - err = k.UpdateAffiliateParameters(ctx, &types.MsgUpdateAffiliateParameters{ - Authority: constants.GovAuthority, - AffiliateParameters: types.AffiliateParameters{ - // Each affiliate can only generate 250_000_000_000 quantums of attributable revenue on a 30d window - Maximum_30DAttributableVolumePerReferredUserQuoteQuantums: 250_000_000_000, - }, - }) - require.NoError(t, err) - }, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - tApp := testapp.NewTestAppBuilder(t).Build() - ctx := tApp.InitChain() - k := tApp.App.AffiliatesKeeper - statsKeeper := tApp.App.StatsKeeper - - err := k.UpdateAffiliateTiers(ctx, types.DefaultAffiliateTiers) - require.NoError(t, err) - - tc.setup(t, ctx, &k, &statsKeeper) - - err = k.AggregateAffiliateReferredVolumeForFills(ctx) - require.NoError(t, err) - - for idx := range tc.expectedReferrer { - referrer := tc.expectedReferrer[idx] - referrerUser := statsKeeper.GetUserStats(ctx, referrer) - require.NoError(t, err) - require.Equal(t, tc.expectedAttributedVolume[idx], referrerUser.Affiliate_30DReferredVolumeQuoteQuantums) - } - }) - } -} - func TestGetTierForAffiliateEmptyTiers(t *testing.T) { tApp := testapp.NewTestAppBuilder(t).Build() ctx := tApp.InitChain() diff --git a/protocol/x/affiliates/types/affiliates.pb.go b/protocol/x/affiliates/types/affiliates.pb.go index 4389973ae2..343b32a932 100644 --- a/protocol/x/affiliates/types/affiliates.pb.go +++ b/protocol/x/affiliates/types/affiliates.pb.go @@ -75,7 +75,8 @@ type AffiliateTiers_Tier struct { // Required all-time referred volume in quote quantums. ReqReferredVolumeQuoteQuantums uint64 `protobuf:"varint,1,opt,name=req_referred_volume_quote_quantums,json=reqReferredVolumeQuoteQuantums,proto3" json:"req_referred_volume_quote_quantums,omitempty"` // Required currently staked native tokens (in whole coins). - ReqStakedWholeCoins uint32 `protobuf:"varint,2,opt,name=req_staked_whole_coins,json=reqStakedWholeCoins,proto3" json:"req_staked_whole_coins,omitempty"` + // This is deprecated + ReqStakedWholeCoins uint32 `protobuf:"varint,2,opt,name=req_staked_whole_coins,json=reqStakedWholeCoins,proto3" json:"req_staked_whole_coins,omitempty"` // Deprecated: Do not use. // Taker fee share in parts-per-million. TakerFeeSharePpm uint32 `protobuf:"varint,3,opt,name=taker_fee_share_ppm,json=takerFeeSharePpm,proto3" json:"taker_fee_share_ppm,omitempty"` } @@ -120,6 +121,7 @@ func (m *AffiliateTiers_Tier) GetReqReferredVolumeQuoteQuantums() uint64 { return 0 } +// Deprecated: Do not use. func (m *AffiliateTiers_Tier) GetReqStakedWholeCoins() uint32 { if m != nil { return m.ReqStakedWholeCoins @@ -364,43 +366,43 @@ func init() { } var fileDescriptor_7de5ba9c426e9350 = []byte{ - // 561 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x54, 0x41, 0x6b, 0x13, 0x41, - 0x14, 0xce, 0xa6, 0x51, 0xe8, 0x88, 0x22, 0x9b, 0xa2, 0x31, 0x94, 0xb5, 0xe4, 0x94, 0x83, 0xd9, - 0x04, 0x23, 0xc5, 0x83, 0x14, 0x13, 0xa1, 0xa8, 0x54, 0x4c, 0x37, 0xda, 0x82, 0x97, 0x61, 0x92, - 0x7d, 0x49, 0x06, 0x77, 0x76, 0x36, 0x33, 0xb3, 0x31, 0xbd, 0x78, 0xf0, 0x17, 0x08, 0xfe, 0x15, - 0xd1, 0xbf, 0xd0, 0x63, 0xf1, 0x24, 0x1e, 0x44, 0x92, 0x3f, 0x22, 0x33, 0xbb, 0x49, 0x36, 0xd8, - 0xa8, 0x97, 0x61, 0xf6, 0x7d, 0xef, 0x7d, 0xf3, 0xbe, 0xf7, 0xcd, 0x0e, 0xaa, 0xfa, 0x67, 0xfe, - 0x34, 0x12, 0x5c, 0xf1, 0x3e, 0x0f, 0xea, 0x64, 0x30, 0xa0, 0x01, 0x25, 0x0a, 0x64, 0x66, 0xeb, - 0x1a, 0xd8, 0xbe, 0x9d, 0xcd, 0x74, 0x57, 0x70, 0xf9, 0x4e, 0x9f, 0x4b, 0xc6, 0x25, 0x36, 0x58, - 0x3d, 0xf9, 0x48, 0x6a, 0xca, 0x3b, 0x43, 0x3e, 0xe4, 0x49, 0x5c, 0xef, 0x92, 0x68, 0xe5, 0x53, - 0x1e, 0xdd, 0x68, 0x2d, 0xea, 0x5f, 0x51, 0x10, 0xd2, 0x7e, 0x8a, 0xae, 0x28, 0xbd, 0x29, 0x59, - 0x7b, 0x5b, 0xd5, 0x6b, 0xf7, 0xef, 0xb9, 0x1b, 0x0e, 0x73, 0xd7, 0xeb, 0x5c, 0xbd, 0xb6, 0x0b, - 0xe7, 0x3f, 0xef, 0xe6, 0xbc, 0x84, 0xa0, 0xfc, 0xc5, 0x42, 0x05, 0x1d, 0xb5, 0x9f, 0xa3, 0x8a, - 0x80, 0x31, 0x16, 0x30, 0x00, 0x21, 0xc0, 0xc7, 0x13, 0x1e, 0xc4, 0x0c, 0xf0, 0x38, 0xe6, 0x4a, - 0xaf, 0x24, 0x54, 0x31, 0xd3, 0xe7, 0x59, 0xd5, 0x82, 0xe7, 0x08, 0x18, 0x7b, 0x69, 0xe2, 0x89, - 0xc9, 0x3b, 0xd6, 0x69, 0xc7, 0x69, 0x96, 0xdd, 0x44, 0xb7, 0x34, 0x97, 0x54, 0xe4, 0x2d, 0xf8, - 0xf8, 0xdd, 0x88, 0x07, 0x80, 0xfb, 0x9c, 0x86, 0xb2, 0x94, 0xdf, 0xb3, 0xaa, 0xd7, 0xbd, 0xa2, - 0x80, 0x71, 0xd7, 0x80, 0xa7, 0x1a, 0x7b, 0xa2, 0x21, 0xbb, 0x86, 0x8a, 0x3a, 0x24, 0xf0, 0x00, - 0x00, 0xcb, 0x11, 0x11, 0x80, 0xa3, 0x88, 0x95, 0xb6, 0x4c, 0xc5, 0x4d, 0x03, 0x1d, 0x02, 0x74, - 0x35, 0xd0, 0x89, 0x58, 0xe5, 0xab, 0x85, 0xec, 0xa5, 0xba, 0xd3, 0x11, 0x55, 0x10, 0x50, 0xa9, - 0xec, 0xa3, 0xf5, 0xc9, 0x34, 0xfe, 0x3d, 0x99, 0x65, 0xed, 0x25, 0xd3, 0xe9, 0xa6, 0xc3, 0xd9, - 0x45, 0xdb, 0xc4, 0xf7, 0x05, 0x48, 0x09, 0x09, 0xf3, 0xb6, 0xb7, 0x0a, 0x6c, 0xea, 0x3c, 0xbf, - 0xa1, 0xf3, 0x1f, 0x79, 0x54, 0x5c, 0x9e, 0xde, 0x21, 0x82, 0x30, 0x50, 0xda, 0xd4, 0x0f, 0x16, - 0x7a, 0xcc, 0xc8, 0x94, 0xb2, 0x98, 0xe1, 0x66, 0xc3, 0xc7, 0x44, 0x29, 0x41, 0x7b, 0xb1, 0x22, - 0xbd, 0x00, 0x16, 0x76, 0x44, 0x20, 0x56, 0x16, 0xc5, 0x12, 0xc4, 0xe5, 0x06, 0x3d, 0x4c, 0x79, - 0x9a, 0x0d, 0xbf, 0x95, 0x61, 0x49, 0xbc, 0xea, 0x80, 0x58, 0x98, 0xf7, 0x5a, 0x82, 0x58, 0xb7, - 0xee, 0x00, 0xed, 0x1a, 0x7e, 0x00, 0xcc, 0x68, 0x68, 0x7a, 0xd1, 0xaa, 0xf4, 0x38, 0x30, 0xf5, - 0xa7, 0xa9, 0xa8, 0x52, 0x9a, 0xf3, 0x22, 0x49, 0x39, 0x04, 0x73, 0xbf, 0x9e, 0xf9, 0x53, 0xfb, - 0x3d, 0x3a, 0x58, 0xd3, 0xb0, 0xd0, 0x89, 0x05, 0x4c, 0x20, 0x8c, 0xff, 0x43, 0xc1, 0x96, 0x51, - 0xb0, 0x9f, 0x51, 0xb0, 0xe0, 0xf0, 0x12, 0x8a, 0xbf, 0xf5, 0x5f, 0x39, 0xca, 0xdc, 0x8a, 0x97, - 0x13, 0x10, 0x82, 0xfa, 0x20, 0xed, 0xfd, 0x3f, 0xfc, 0x6b, 0x97, 0xbe, 0x7d, 0xae, 0xed, 0xa4, - 0x7f, 0x5f, 0x2b, 0xc1, 0xba, 0x4a, 0xd0, 0x70, 0x98, 0x71, 0xb6, 0x7d, 0x72, 0x3e, 0x73, 0xac, - 0x8b, 0x99, 0x63, 0xfd, 0x9a, 0x39, 0xd6, 0xc7, 0xb9, 0x93, 0xbb, 0x98, 0x3b, 0xb9, 0xef, 0x73, - 0x27, 0xf7, 0xe6, 0xd1, 0x90, 0xaa, 0x51, 0xdc, 0x73, 0xfb, 0x9c, 0xd5, 0xd7, 0xde, 0x84, 0xc9, - 0x83, 0x5a, 0x7f, 0x44, 0x68, 0x58, 0x5f, 0x46, 0xa6, 0xd9, 0x77, 0x42, 0x9d, 0x45, 0x20, 0x7b, - 0x57, 0x0d, 0xd8, 0xfc, 0x1d, 0x00, 0x00, 0xff, 0xff, 0x10, 0x28, 0x41, 0x19, 0x4f, 0x04, 0x00, - 0x00, + // 567 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x54, 0x41, 0x8f, 0xd2, 0x40, + 0x14, 0xa6, 0x80, 0x26, 0x3b, 0x46, 0x63, 0xca, 0x46, 0x2b, 0xd9, 0xd4, 0x0d, 0x27, 0x0e, 0x52, + 0x88, 0x98, 0xd5, 0x83, 0xd9, 0x08, 0x26, 0x1b, 0x35, 0x6b, 0x64, 0x8b, 0xee, 0x26, 0x5e, 0x26, + 0x03, 0x7d, 0xc0, 0xc4, 0x4e, 0xa7, 0xcc, 0x4c, 0x91, 0xbd, 0x78, 0xf0, 0x17, 0x78, 0xf3, 0x8f, + 0x98, 0x98, 0xf8, 0x0b, 0xf6, 0xb8, 0xf1, 0x64, 0x3c, 0x18, 0x03, 0x7f, 0xc4, 0x4c, 0x5b, 0xa0, + 0xc4, 0x45, 0xf7, 0xd2, 0x0c, 0xef, 0xfb, 0xde, 0x37, 0xef, 0x7b, 0x5f, 0x29, 0xaa, 0x7a, 0xa7, + 0xde, 0x34, 0x14, 0x5c, 0xf1, 0x3e, 0xf7, 0xeb, 0x64, 0x30, 0xa0, 0x3e, 0x25, 0x0a, 0x64, 0xe6, + 0xe8, 0xc4, 0xb0, 0x79, 0x3b, 0xcb, 0x74, 0x56, 0x70, 0xf9, 0x4e, 0x9f, 0x4b, 0xc6, 0x25, 0x8e, + 0xb1, 0x7a, 0xf2, 0x23, 0xe9, 0x29, 0x6f, 0x0f, 0xf9, 0x90, 0x27, 0x75, 0x7d, 0x4a, 0xaa, 0x95, + 0xcf, 0x79, 0x74, 0xa3, 0xb5, 0xe8, 0x7f, 0x4d, 0x41, 0x48, 0xf3, 0x19, 0xba, 0xa2, 0xf4, 0xc1, + 0x32, 0x76, 0x0b, 0xd5, 0x6b, 0xf7, 0xef, 0x39, 0x1b, 0x2e, 0x73, 0xd6, 0xfb, 0x1c, 0xfd, 0x6c, + 0x17, 0xcf, 0x7e, 0xdd, 0xcd, 0xb9, 0x89, 0x40, 0xf9, 0x9b, 0x81, 0x8a, 0xba, 0x6a, 0xbe, 0x40, + 0x15, 0x01, 0x63, 0x2c, 0x60, 0x00, 0x42, 0x80, 0x87, 0x27, 0xdc, 0x8f, 0x18, 0xe0, 0x71, 0xc4, + 0x95, 0x7e, 0x92, 0x40, 0x45, 0x4c, 0xdf, 0x67, 0x54, 0x8b, 0xae, 0x2d, 0x60, 0xec, 0xa6, 0xc4, + 0xe3, 0x98, 0x77, 0xa4, 0x69, 0x47, 0x29, 0xcb, 0x7c, 0x88, 0x6e, 0x69, 0x2d, 0xa9, 0xc8, 0x3b, + 0xf0, 0xf0, 0xfb, 0x11, 0xf7, 0x01, 0xf7, 0x39, 0x0d, 0xa4, 0x95, 0xdf, 0x35, 0xaa, 0xd7, 0xdb, + 0x79, 0xcb, 0x70, 0x4b, 0x02, 0xc6, 0xdd, 0x98, 0x70, 0xa2, 0xf1, 0xa7, 0x1a, 0x36, 0x6b, 0xa8, + 0xa4, 0x4b, 0x02, 0x0f, 0x00, 0xb0, 0x1c, 0x11, 0x01, 0x38, 0x0c, 0x99, 0x55, 0xd0, 0x5d, 0xee, + 0xcd, 0x18, 0x3a, 0x00, 0xe8, 0x6a, 0xa0, 0x13, 0xb2, 0xca, 0x57, 0x03, 0x99, 0x4b, 0x87, 0x27, + 0x23, 0xaa, 0xc0, 0xa7, 0x52, 0x99, 0x87, 0xeb, 0xdb, 0x69, 0xfc, 0x7f, 0x3b, 0xcb, 0xde, 0x0b, + 0x36, 0xd4, 0x4d, 0x17, 0xb4, 0x83, 0xb6, 0x88, 0xe7, 0x09, 0x90, 0x12, 0x12, 0xe5, 0x2d, 0x77, + 0x55, 0xd8, 0x34, 0x79, 0x7e, 0xc3, 0xe4, 0x3f, 0xf3, 0xa8, 0xb4, 0xbc, 0xbd, 0x43, 0x04, 0x61, + 0xa0, 0x74, 0xb0, 0x1f, 0x0d, 0xf4, 0x84, 0x91, 0x29, 0x65, 0x11, 0xc3, 0xcd, 0x86, 0x87, 0x89, + 0x52, 0x82, 0xf6, 0x22, 0x45, 0x7a, 0x3e, 0x2c, 0x22, 0x09, 0x41, 0xac, 0x62, 0x8a, 0x24, 0x88, + 0x8b, 0x43, 0x7a, 0x94, 0xea, 0x34, 0x1b, 0x5e, 0x2b, 0xa3, 0x92, 0xe4, 0xd5, 0x01, 0xb1, 0x08, + 0xf0, 0x8d, 0x04, 0xb1, 0x1e, 0xdf, 0x3e, 0xda, 0x89, 0xf5, 0x01, 0x30, 0xa3, 0x41, 0x3c, 0x8b, + 0x76, 0xa5, 0xd7, 0x81, 0xa9, 0x37, 0x4d, 0x4d, 0x59, 0x29, 0xe7, 0x65, 0x42, 0x39, 0x80, 0xf8, + 0x1d, 0x7b, 0xee, 0x4d, 0xcd, 0x0f, 0x68, 0x7f, 0xcd, 0xc3, 0xc2, 0x27, 0x16, 0x30, 0x81, 0x20, + 0xba, 0x84, 0x83, 0x42, 0xec, 0x60, 0x2f, 0xe3, 0x60, 0xa1, 0xe1, 0x26, 0x12, 0xff, 0x9a, 0xbf, + 0x72, 0x98, 0x79, 0x2b, 0x5e, 0x4d, 0x40, 0x08, 0xea, 0x81, 0x34, 0xf7, 0xfe, 0xca, 0xaf, 0x6d, + 0x7d, 0xff, 0x52, 0xdb, 0x4e, 0xff, 0x81, 0xad, 0x04, 0xeb, 0x2a, 0x41, 0x83, 0x61, 0x26, 0xd9, + 0xf6, 0xf1, 0xd9, 0xcc, 0x36, 0xce, 0x67, 0xb6, 0xf1, 0x7b, 0x66, 0x1b, 0x9f, 0xe6, 0x76, 0xee, + 0x7c, 0x6e, 0xe7, 0x7e, 0xcc, 0xed, 0xdc, 0xdb, 0xc7, 0x43, 0xaa, 0x46, 0x51, 0xcf, 0xe9, 0x73, + 0x56, 0x5f, 0xfb, 0x2e, 0x4c, 0x1e, 0xd4, 0xfa, 0x23, 0x42, 0x83, 0xfa, 0xb2, 0x32, 0xcd, 0x7e, + 0x2b, 0xd4, 0x69, 0x08, 0xb2, 0x77, 0x35, 0x06, 0x9b, 0x7f, 0x02, 0x00, 0x00, 0xff, 0xff, 0xe3, + 0x02, 0x97, 0xa7, 0x53, 0x04, 0x00, 0x00, } func (m *AffiliateTiers) Marshal() (dAtA []byte, err error) { diff --git a/protocol/x/affiliates/types/expected_keepers.go b/protocol/x/affiliates/types/expected_keepers.go index bc7ccf321b..4de8a55b4d 100644 --- a/protocol/x/affiliates/types/expected_keepers.go +++ b/protocol/x/affiliates/types/expected_keepers.go @@ -12,6 +12,8 @@ type StatsKeeper interface { GetBlockStats(ctx sdk.Context) *stattypes.BlockStats GetUserStats(ctx sdk.Context, address string) *stattypes.UserStats SetUserStats(ctx sdk.Context, address string, userStats *stattypes.UserStats) + GetEpochStatsOrNil(ctx sdk.Context, epoch uint32) *stattypes.EpochStats + SetEpochStats(ctx sdk.Context, epoch uint32, epochStats *stattypes.EpochStats) } type FeetiersKeeper interface { diff --git a/protocol/x/affiliates/types/query.pb.go b/protocol/x/affiliates/types/query.pb.go index 58e529a11a..305363f9b3 100644 --- a/protocol/x/affiliates/types/query.pb.go +++ b/protocol/x/affiliates/types/query.pb.go @@ -94,6 +94,8 @@ type AffiliateInfoResponse struct { StakedAmount github_com_dydxprotocol_v4_chain_protocol_dtypes.SerializableInt `protobuf:"bytes,5,opt,name=staked_amount,json=stakedAmount,proto3,customtype=github.com/dydxprotocol/v4-chain/protocol/dtypes.SerializableInt" json:"staked_amount"` // The affiliate's 30d referred volume in quote quantums. ReferredVolume_30DRolling github_com_dydxprotocol_v4_chain_protocol_dtypes.SerializableInt `protobuf:"bytes,6,opt,name=referred_volume_30d_rolling,json=referredVolume30dRolling,proto3,customtype=github.com/dydxprotocol/v4-chain/protocol/dtypes.SerializableInt" json:"referred_volume_30d_rolling"` + // The affiliate's 30d attributed volume in quote quantums (from referees). + AttributedVolume_30DRolling github_com_dydxprotocol_v4_chain_protocol_dtypes.SerializableInt `protobuf:"bytes,7,opt,name=attributed_volume_30d_rolling,json=attributedVolume30dRolling,proto3,customtype=github.com/dydxprotocol/v4-chain/protocol/dtypes.SerializableInt" json:"attributed_volume_30d_rolling"` } func (m *AffiliateInfoResponse) Reset() { *m = AffiliateInfoResponse{} } @@ -599,59 +601,60 @@ func init() { } var fileDescriptor_2edc1b3ea39b05a9 = []byte{ - // 822 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x95, 0xcf, 0x6f, 0xd3, 0x48, - 0x14, 0xc7, 0xe3, 0xfe, 0xda, 0x76, 0xda, 0x74, 0xb7, 0xb3, 0x5d, 0xad, 0x9b, 0x56, 0x69, 0xe5, - 0xd5, 0x6a, 0xa3, 0x6d, 0x63, 0xb7, 0x49, 0x77, 0x25, 0x24, 0x0e, 0x24, 0x08, 0x41, 0x7b, 0x69, - 0x71, 0x51, 0x91, 0xe0, 0x60, 0x9c, 0x78, 0x92, 0x8c, 0xb0, 0x3d, 0xae, 0xc7, 0x29, 0x0d, 0x88, - 0x0b, 0x17, 0xae, 0x48, 0x9c, 0xf9, 0x03, 0x90, 0x38, 0x20, 0x81, 0xc4, 0xbf, 0xd0, 0x63, 0x05, - 0x17, 0xc4, 0xa1, 0x42, 0x6d, 0xff, 0x10, 0x14, 0x7b, 0x3c, 0x76, 0x9a, 0x44, 0x71, 0x44, 0x6f, - 0xce, 0x7b, 0xf3, 0xde, 0xf7, 0xe3, 0x97, 0xf9, 0x3e, 0x83, 0xbf, 0x8c, 0x96, 0x71, 0xe4, 0xb8, - 0xc4, 0x23, 0x55, 0x62, 0x2a, 0x7a, 0xad, 0x86, 0x4d, 0xac, 0x7b, 0x88, 0x2a, 0x07, 0x4d, 0xe4, - 0xb6, 0x64, 0x3f, 0x03, 0xff, 0x8c, 0x1f, 0x92, 0xa3, 0x43, 0x99, 0x85, 0x2a, 0xa1, 0x16, 0xa1, - 0x9a, 0x9f, 0x53, 0x82, 0x1f, 0x41, 0x4d, 0x66, 0xbe, 0x4e, 0xea, 0x24, 0x88, 0xb7, 0x9f, 0x58, - 0x34, 0xd7, 0x4f, 0x2e, 0x7a, 0x64, 0x27, 0x97, 0xea, 0x84, 0xd4, 0x4d, 0xa4, 0xe8, 0x0e, 0x56, - 0x74, 0xdb, 0x26, 0x9e, 0xee, 0x61, 0x62, 0xb3, 0xac, 0xb4, 0x0d, 0xe6, 0x4b, 0x61, 0xc5, 0x96, - 0x5d, 0x23, 0x2a, 0x3a, 0x68, 0x22, 0xea, 0xc1, 0x02, 0xf8, 0x45, 0x37, 0x0c, 0x17, 0x51, 0x2a, - 0x0a, 0x2b, 0x42, 0x6e, 0xaa, 0x2c, 0x7e, 0xfe, 0x98, 0x9f, 0x67, 0x60, 0xa5, 0x20, 0xb3, 0xe7, - 0xb9, 0xd8, 0xae, 0xab, 0xe1, 0x41, 0xe9, 0x62, 0x14, 0xfc, 0x71, 0xa9, 0x19, 0x75, 0x88, 0x4d, - 0x11, 0xfc, 0x1b, 0xcc, 0x62, 0xaa, 0x3d, 0x69, 0x60, 0x0f, 0x99, 0x98, 0x7a, 0xc8, 0xf0, 0x9b, - 0x4e, 0xaa, 0x69, 0x4c, 0xef, 0x47, 0x41, 0x08, 0xc1, 0x98, 0x87, 0x91, 0x2b, 0x8e, 0xac, 0x08, - 0xb9, 0xb4, 0xea, 0x3f, 0x43, 0x09, 0xa4, 0x6b, 0x08, 0x69, 0xb4, 0xa1, 0xbb, 0x48, 0x73, 0x1c, - 0x4b, 0x1c, 0xf5, 0x93, 0xd3, 0x35, 0x84, 0xf6, 0xda, 0xb1, 0x5d, 0xc7, 0x82, 0x14, 0xfc, 0xea, - 0xa2, 0x1a, 0x72, 0x5d, 0x64, 0x68, 0x87, 0xc4, 0x6c, 0x5a, 0x48, 0x1c, 0x5b, 0x11, 0x72, 0x33, - 0xe5, 0xed, 0xe3, 0xd3, 0xe5, 0xd4, 0xb7, 0xd3, 0xe5, 0x1b, 0x75, 0xec, 0x35, 0x9a, 0x15, 0xb9, - 0x4a, 0x2c, 0xa5, 0x63, 0x70, 0x87, 0x9b, 0xf9, 0x6a, 0x43, 0xc7, 0xb6, 0xc2, 0x23, 0x86, 0xd7, - 0x72, 0x10, 0x95, 0xf7, 0x90, 0x8b, 0x75, 0x13, 0x3f, 0xd5, 0x2b, 0x26, 0xda, 0xb2, 0x3d, 0x51, - 0x50, 0x67, 0x43, 0x89, 0x7d, 0x5f, 0x01, 0x5a, 0x20, 0x4d, 0x3d, 0xfd, 0x31, 0x32, 0x34, 0xdd, - 0x22, 0x4d, 0xdb, 0x13, 0xc7, 0x7d, 0xc9, 0x3b, 0x57, 0x25, 0xa9, 0xce, 0x04, 0xed, 0x4b, 0x7e, - 0x77, 0xf8, 0x52, 0x00, 0x8b, 0x97, 0x5e, 0x52, 0x2b, 0xae, 0x1b, 0x9a, 0x4b, 0x4c, 0x13, 0xdb, - 0x75, 0x71, 0xe2, 0x8a, 0xd5, 0xc5, 0xce, 0xd7, 0x2d, 0xae, 0x1b, 0x6a, 0xa0, 0x24, 0xdd, 0x06, - 0x73, 0x2a, 0xcb, 0x95, 0x5b, 0x3f, 0x73, 0x5f, 0x1e, 0x02, 0x18, 0x6f, 0xc4, 0xee, 0xca, 0x2d, - 0x30, 0xc7, 0xef, 0xb0, 0x96, 0xb4, 0xe7, 0x6f, 0xbc, 0x84, 0xc5, 0xa5, 0x0c, 0x10, 0x4b, 0xa6, - 0xc9, 0xaf, 0xe3, 0x3d, 0x8c, 0x5c, 0xca, 0x60, 0xa5, 0x47, 0x60, 0xa1, 0x47, 0x8e, 0xe9, 0xdf, - 0x04, 0xe3, 0xed, 0x8b, 0x17, 0x68, 0x4e, 0x17, 0xfe, 0x91, 0xfb, 0x78, 0x56, 0xee, 0xac, 0x2f, - 0x8f, 0xb5, 0x47, 0xaf, 0x06, 0xb5, 0xd2, 0x22, 0x58, 0xe0, 0x69, 0x7e, 0xc3, 0x43, 0x79, 0x0b, - 0x64, 0x7a, 0x25, 0x99, 0xfe, 0x0e, 0x98, 0xe2, 0x46, 0x61, 0x0c, 0xab, 0x83, 0x19, 0x78, 0x1f, - 0xc6, 0x11, 0xf5, 0xe8, 0x60, 0xd9, 0x39, 0x44, 0xae, 0x8b, 0x0d, 0x44, 0x7b, 0xb1, 0xc4, 0x92, - 0x11, 0x0b, 0x09, 0x83, 0xc9, 0x59, 0x78, 0x9f, 0x90, 0x85, 0xf7, 0x90, 0x96, 0x62, 0x72, 0xbb, - 0xba, 0xab, 0x5b, 0xc8, 0x8b, 0xfd, 0x2f, 0x07, 0x60, 0xb1, 0x67, 0x96, 0xd1, 0xa8, 0x00, 0x38, - 0x3c, 0xca, 0x70, 0xd6, 0x06, 0xe3, 0x44, 0x9d, 0x18, 0x4f, 0xac, 0x4b, 0xe1, 0xdd, 0x24, 0x18, - 0xbf, 0xdb, 0xde, 0xd0, 0xf0, 0xad, 0x00, 0xd2, 0x1d, 0xdb, 0x0b, 0xe6, 0x07, 0xf7, 0x8e, 0xad, - 0xcc, 0x8c, 0x9c, 0xf4, 0x78, 0xf0, 0x3a, 0xd2, 0xb5, 0x17, 0x5f, 0x2e, 0x5e, 0x8f, 0x14, 0xe1, - 0x86, 0x32, 0x70, 0x97, 0x6b, 0xd8, 0xae, 0x11, 0xe5, 0x19, 0x73, 0xc3, 0x73, 0xf8, 0x46, 0x00, - 0x20, 0xb2, 0x0e, 0xfc, 0xb7, 0xaf, 0x72, 0x97, 0x51, 0x33, 0xab, 0x89, 0xce, 0x32, 0xc4, 0xff, - 0x7d, 0xc4, 0x75, 0x28, 0xf7, 0x45, 0xe4, 0x2b, 0xa9, 0xd2, 0x8a, 0xf1, 0xbd, 0x17, 0xc0, 0x5c, - 0x97, 0xc3, 0xe0, 0x46, 0xff, 0x01, 0xf5, 0x71, 0x6a, 0xa6, 0x30, 0x4c, 0x09, 0x83, 0xde, 0xf4, - 0xa1, 0x65, 0xb8, 0xd6, 0x7f, 0xae, 0xa6, 0xa9, 0x45, 0xb3, 0xf5, 0x1d, 0x0b, 0x3f, 0x08, 0x00, - 0x76, 0xbb, 0x09, 0x16, 0x86, 0xb0, 0x5e, 0x08, 0x5d, 0x1c, 0xaa, 0x26, 0x39, 0x35, 0x27, 0xe6, - 0xde, 0x86, 0x9f, 0x04, 0xf0, 0x7b, 0x8f, 0x8b, 0x0e, 0x8b, 0xc3, 0xd8, 0x22, 0xe4, 0xde, 0x1c, - 0xae, 0x88, 0x81, 0xff, 0xe7, 0x83, 0x2b, 0x30, 0x9f, 0x00, 0x3c, 0x32, 0x5e, 0xe7, 0xbc, 0xf9, - 0xc6, 0x48, 0x32, 0xef, 0xcb, 0x3b, 0x2c, 0xc9, 0xbc, 0xbb, 0x56, 0xdb, 0x50, 0xf3, 0xe6, 0xfb, - 0xab, 0xbc, 0x7f, 0x7c, 0x96, 0x15, 0x4e, 0xce, 0xb2, 0xc2, 0xf7, 0xb3, 0xac, 0xf0, 0xea, 0x3c, - 0x9b, 0x3a, 0x39, 0xcf, 0xa6, 0xbe, 0x9e, 0x67, 0x53, 0x0f, 0xae, 0x27, 0xff, 0xe2, 0x1e, 0xc5, - 0x55, 0xfc, 0xaf, 0x6f, 0x65, 0xc2, 0x4f, 0x16, 0x7f, 0x04, 0x00, 0x00, 0xff, 0xff, 0xd4, 0x38, - 0xfa, 0x6c, 0x46, 0x0a, 0x00, 0x00, + // 843 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x95, 0xcd, 0x6e, 0xeb, 0x44, + 0x14, 0xc7, 0xe3, 0x4b, 0x7a, 0x3f, 0xe6, 0x36, 0x85, 0x0e, 0x45, 0xb8, 0x6e, 0x49, 0x2b, 0x23, + 0x44, 0x44, 0x1b, 0xbb, 0x4d, 0x0a, 0x12, 0x12, 0x0b, 0x12, 0x84, 0xa0, 0xdd, 0xb4, 0xb8, 0xa8, + 0x48, 0xb0, 0x30, 0x4e, 0x3c, 0x49, 0x46, 0xd8, 0x1e, 0x77, 0x66, 0x52, 0x1a, 0x10, 0x1b, 0x36, + 0x2c, 0xd8, 0x20, 0xb1, 0xe6, 0x01, 0x90, 0x58, 0x20, 0x81, 0xc4, 0x2b, 0x74, 0x59, 0xc1, 0x06, + 0xb1, 0xa8, 0x50, 0xcb, 0x13, 0xf0, 0x04, 0x28, 0xf6, 0x78, 0xec, 0x7c, 0x29, 0x8e, 0xe8, 0xce, + 0x39, 0x67, 0xce, 0xf9, 0xff, 0x7c, 0xe2, 0xff, 0x19, 0xf0, 0xb2, 0x3b, 0x70, 0x2f, 0x43, 0x4a, + 0x38, 0x69, 0x13, 0xcf, 0x74, 0x3a, 0x1d, 0xec, 0x61, 0x87, 0x23, 0x66, 0x9e, 0xf7, 0x11, 0x1d, + 0x18, 0x51, 0x06, 0xbe, 0x98, 0x3d, 0x64, 0xa4, 0x87, 0xb4, 0xf5, 0x36, 0x61, 0x3e, 0x61, 0x76, + 0x94, 0x33, 0xe3, 0x1f, 0x71, 0x8d, 0xb6, 0xd6, 0x25, 0x5d, 0x12, 0xc7, 0x87, 0x4f, 0x22, 0x5a, + 0x99, 0x25, 0x97, 0x3e, 0x8a, 0x93, 0x9b, 0x5d, 0x42, 0xba, 0x1e, 0x32, 0x9d, 0x10, 0x9b, 0x4e, + 0x10, 0x10, 0xee, 0x70, 0x4c, 0x02, 0x91, 0xd5, 0x8f, 0xc0, 0x5a, 0x23, 0xa9, 0x38, 0x0c, 0x3a, + 0xc4, 0x42, 0xe7, 0x7d, 0xc4, 0x38, 0xac, 0x81, 0x47, 0x8e, 0xeb, 0x52, 0xc4, 0x98, 0xaa, 0x6c, + 0x2b, 0x95, 0x27, 0x4d, 0xf5, 0xf7, 0x5f, 0xab, 0x6b, 0x02, 0xac, 0x11, 0x67, 0x4e, 0x39, 0xc5, + 0x41, 0xd7, 0x4a, 0x0e, 0xea, 0xff, 0x16, 0xc1, 0x0b, 0x63, 0xcd, 0x58, 0x48, 0x02, 0x86, 0xe0, + 0x2b, 0x60, 0x05, 0x33, 0xfb, 0xf3, 0x1e, 0xe6, 0xc8, 0xc3, 0x8c, 0x23, 0x37, 0x6a, 0xfa, 0xd8, + 0x2a, 0x61, 0xf6, 0x51, 0x1a, 0x84, 0x10, 0x14, 0x39, 0x46, 0x54, 0x7d, 0xb0, 0xad, 0x54, 0x4a, + 0x56, 0xf4, 0x0c, 0x75, 0x50, 0xea, 0x20, 0x64, 0xb3, 0x9e, 0x43, 0x91, 0x1d, 0x86, 0xbe, 0xfa, + 0x4c, 0x94, 0x7c, 0xda, 0x41, 0xe8, 0x74, 0x18, 0x3b, 0x09, 0x7d, 0xc8, 0xc0, 0xb3, 0x14, 0x75, + 0x10, 0xa5, 0xc8, 0xb5, 0x2f, 0x88, 0xd7, 0xf7, 0x91, 0x5a, 0xdc, 0x56, 0x2a, 0xcb, 0xcd, 0xa3, + 0xab, 0x9b, 0xad, 0xc2, 0x5f, 0x37, 0x5b, 0x6f, 0x77, 0x31, 0xef, 0xf5, 0x5b, 0x46, 0x9b, 0xf8, + 0xe6, 0xc8, 0xe0, 0x2e, 0x0e, 0xaa, 0xed, 0x9e, 0x83, 0x03, 0x53, 0x46, 0x5c, 0x3e, 0x08, 0x11, + 0x33, 0x4e, 0x11, 0xc5, 0x8e, 0x87, 0xbf, 0x70, 0x5a, 0x1e, 0x3a, 0x0c, 0xb8, 0xaa, 0x58, 0x2b, + 0x89, 0xc4, 0x59, 0xa4, 0x00, 0x7d, 0x50, 0x62, 0xdc, 0xf9, 0x0c, 0xb9, 0xb6, 0xe3, 0x93, 0x7e, + 0xc0, 0xd5, 0xa5, 0x48, 0xf2, 0xfd, 0xfb, 0x92, 0xb4, 0x96, 0xe3, 0xf6, 0x8d, 0xa8, 0x3b, 0xfc, + 0x46, 0x01, 0x1b, 0x63, 0x2f, 0x69, 0xd7, 0xf7, 0x5c, 0x9b, 0x12, 0xcf, 0xc3, 0x41, 0x57, 0x7d, + 0x78, 0xcf, 0xea, 0xea, 0xe8, 0xeb, 0xd6, 0xf7, 0x5c, 0x2b, 0x56, 0x82, 0xdf, 0x2a, 0xe0, 0x25, + 0x87, 0x73, 0x8a, 0x5b, 0x7d, 0x3e, 0x9d, 0xe5, 0xd1, 0x3d, 0xb3, 0x68, 0xa9, 0xdc, 0x38, 0x8d, + 0xfe, 0x1e, 0x58, 0xb5, 0x04, 0x69, 0x73, 0xf0, 0x7f, 0xbe, 0xde, 0x4f, 0x00, 0xcc, 0x36, 0x12, + 0x5f, 0xee, 0xbb, 0x60, 0x55, 0x3a, 0xca, 0xce, 0xdb, 0xf3, 0x39, 0x59, 0x22, 0xe2, 0xba, 0x06, + 0xd4, 0x86, 0xe7, 0x49, 0x73, 0x7c, 0x88, 0x11, 0x65, 0x02, 0x56, 0xff, 0x14, 0xac, 0x4f, 0xc9, + 0x09, 0xfd, 0x77, 0xc0, 0xd2, 0xd0, 0x06, 0xb1, 0xe6, 0xd3, 0xda, 0xab, 0xc6, 0x8c, 0x0d, 0x62, + 0x8c, 0xd6, 0x37, 0x8b, 0xc3, 0xe1, 0x5b, 0x71, 0xad, 0xbe, 0x01, 0xd6, 0x65, 0x5a, 0xfa, 0x2d, + 0x91, 0xf7, 0x81, 0x36, 0x2d, 0x29, 0xf4, 0x8f, 0xc1, 0x13, 0x69, 0x5b, 0xc1, 0xb0, 0x33, 0x9f, + 0x41, 0xf6, 0x11, 0x1c, 0x69, 0x8f, 0x11, 0x96, 0xe3, 0x0b, 0x44, 0x29, 0x76, 0x11, 0x9b, 0xc6, + 0x92, 0x49, 0xa6, 0x2c, 0x24, 0x09, 0xe6, 0x67, 0x91, 0x7d, 0x12, 0x16, 0xd9, 0x43, 0xdf, 0xcc, + 0xc8, 0x9d, 0x38, 0xd4, 0xf1, 0x11, 0xcf, 0xfc, 0x2f, 0xe7, 0x60, 0x63, 0x6a, 0x56, 0xd0, 0x58, + 0x00, 0x84, 0x32, 0x2a, 0x70, 0x76, 0xe7, 0xe3, 0xa4, 0x9d, 0x04, 0x4f, 0xa6, 0x4b, 0xed, 0xa7, + 0xc7, 0x60, 0xe9, 0x83, 0xe1, 0x7d, 0x01, 0x7f, 0x54, 0x40, 0x69, 0x64, 0x97, 0xc2, 0xea, 0xfc, + 0xde, 0x99, 0x05, 0xae, 0x19, 0x79, 0x8f, 0xc7, 0xaf, 0xa3, 0xbf, 0xf9, 0xf5, 0x1f, 0xff, 0x7c, + 0xff, 0xa0, 0x0e, 0xf7, 0xcd, 0xb9, 0x37, 0x8b, 0x8d, 0x83, 0x0e, 0x31, 0xbf, 0x14, 0x6e, 0xf8, + 0x0a, 0xfe, 0xa0, 0x00, 0x90, 0x5a, 0x07, 0xbe, 0x36, 0x53, 0x79, 0xc2, 0xa8, 0xda, 0x4e, 0xae, + 0xb3, 0x02, 0xf1, 0x8d, 0x08, 0x71, 0x0f, 0x1a, 0x33, 0x11, 0xe5, 0x82, 0x6c, 0x0d, 0x32, 0x7c, + 0x3f, 0x2b, 0x60, 0x75, 0xc2, 0x61, 0x70, 0x7f, 0xf6, 0x80, 0x66, 0x38, 0x55, 0xab, 0x2d, 0x52, + 0x22, 0xa0, 0x0f, 0x22, 0x68, 0x03, 0xee, 0xce, 0x9e, 0xab, 0xe7, 0xd9, 0xe9, 0x6c, 0x23, 0xc7, + 0xc2, 0x5f, 0x14, 0x00, 0x27, 0xdd, 0x04, 0x6b, 0x0b, 0x58, 0x2f, 0x81, 0xae, 0x2f, 0x54, 0x93, + 0x9f, 0x5a, 0x12, 0x4b, 0x6f, 0xc3, 0xdf, 0x14, 0xf0, 0xfc, 0x94, 0x0f, 0x1d, 0xd6, 0x17, 0xb1, + 0x45, 0xc2, 0x7d, 0xb0, 0x58, 0x91, 0x00, 0x7f, 0x3d, 0x02, 0x37, 0x61, 0x35, 0x07, 0x78, 0x6a, + 0xbc, 0xd1, 0x79, 0xcb, 0x8d, 0x91, 0x67, 0xde, 0xe3, 0x3b, 0x2c, 0xcf, 0xbc, 0x27, 0x56, 0xdb, + 0x42, 0xf3, 0x96, 0xfb, 0xab, 0x79, 0x76, 0x75, 0x5b, 0x56, 0xae, 0x6f, 0xcb, 0xca, 0xdf, 0xb7, + 0x65, 0xe5, 0xbb, 0xbb, 0x72, 0xe1, 0xfa, 0xae, 0x5c, 0xf8, 0xf3, 0xae, 0x5c, 0xf8, 0xf8, 0xad, + 0xfc, 0x77, 0xee, 0x65, 0x56, 0x25, 0xba, 0x7f, 0x5b, 0x0f, 0xa3, 0x64, 0xfd, 0xbf, 0x00, 0x00, + 0x00, 0xff, 0xff, 0x88, 0x2f, 0xce, 0x39, 0xd4, 0x0a, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. @@ -976,6 +979,16 @@ func (m *AffiliateInfoResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + { + size := m.AttributedVolume_30DRolling.Size() + i -= size + if _, err := m.AttributedVolume_30DRolling.MarshalTo(dAtA[i:]); err != nil { + return 0, err + } + i = encodeVarintQuery(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x3a { size := m.ReferredVolume_30DRolling.Size() i -= size @@ -1358,6 +1371,8 @@ func (m *AffiliateInfoResponse) Size() (n int) { n += 1 + l + sovQuery(uint64(l)) l = m.ReferredVolume_30DRolling.Size() n += 1 + l + sovQuery(uint64(l)) + l = m.AttributedVolume_30DRolling.Size() + n += 1 + l + sovQuery(uint64(l)) return n } @@ -1741,6 +1756,39 @@ func (m *AffiliateInfoResponse) Unmarshal(dAtA []byte) error { return err } iNdEx = postIndex + case 7: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field AttributedVolume_30DRolling", wireType) + } + var byteLen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowQuery + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + byteLen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if byteLen < 0 { + return ErrInvalidLengthQuery + } + postIndex := iNdEx + byteLen + if postIndex < 0 { + return ErrInvalidLengthQuery + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := m.AttributedVolume_30DRolling.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipQuery(dAtA[iNdEx:]) diff --git a/protocol/x/clob/keeper/process_single_match.go b/protocol/x/clob/keeper/process_single_match.go index 9b6cf65ac2..6f0567f20f 100644 --- a/protocol/x/clob/keeper/process_single_match.go +++ b/protocol/x/clob/keeper/process_single_match.go @@ -16,6 +16,7 @@ import ( assettypes "github.com/dydxprotocol/v4-chain/protocol/x/assets/types" "github.com/dydxprotocol/v4-chain/protocol/x/clob/types" revsharetypes "github.com/dydxprotocol/v4-chain/protocol/x/revshare/types" + statstypes "github.com/dydxprotocol/v4-chain/protocol/x/stats/types" satypes "github.com/dydxprotocol/v4-chain/protocol/x/subaccounts/types" gometrics "github.com/hashicorp/go-metrics" ) @@ -576,12 +577,21 @@ func (k Keeper) persistMatchedOrders( revSharesForFill, ) + attributableVolumeAttributions := k.buildAttributableVolumeAttributions( + ctx, + revSharesForFill, + bigFillQuoteQuantums, + matchWithOrders, + affiliateParameters, + ) + k.statsKeeper.RecordFill( ctx, matchWithOrders.TakerOrder.GetSubaccountId().Owner, matchWithOrders.MakerOrder.GetSubaccountId().Owner, bigFillQuoteQuantums, affiliateRevSharesQuoteQuantums, + attributableVolumeAttributions, ) takerOrderRouterFeeQuoteQuantums := big.NewInt(0) @@ -629,6 +639,99 @@ func (k Keeper) persistMatchedOrders( return takerUpdateResult, makerUpdateResult, affiliateRevSharesQuoteQuantums, nil } +// getAttributableVolume calculates the attributable volume for a referee based on their +// already-attributed volume in the last 30 days and the maximum attributable volume cap. +// This does not modify any state. +func (k Keeper) getAttributableVolume( + ctx sdk.Context, + referee string, + volume uint64, + affiliateParameters affiliatetypes.AffiliateParameters, +) uint64 { + // Get the user stats from the referee + refereeUserStats := k.statsKeeper.GetUserStats(ctx, referee) + if refereeUserStats == nil { + return 0 + } + + // Use the ATTRIBUTED volume (how much has already been attributed to their affiliate) + // NOT total trading volume (TakerNotional + MakerNotional) + previouslyAttributedVolume := refereeUserStats.Affiliate_30DAttributedVolumeQuoteQuantums + + // If parameter is 0 then no limit is applied + cap := affiliateParameters.Maximum_30DAttributableVolumePerReferredUserQuoteQuantums + if cap == 0 { + return volume + } + + if previouslyAttributedVolume >= cap { + return 0 + } else if previouslyAttributedVolume+volume > cap { + // Remainder of the volume to get them to the cap + return cap - previouslyAttributedVolume + } + + return volume +} + +func (k Keeper) buildAttributableVolumeAttributions( + ctx sdk.Context, + revSharesForFill revsharetypes.RevSharesForFill, + bigFillQuoteQuantums *big.Int, + matchWithOrders *types.MatchWithOrders, + affiliateParameters affiliatetypes.AffiliateParameters, +) []*statstypes.AffiliateAttribution { + // Build affiliate revenue attributions array (can include both taker and maker) + var affiliateRevenueAttributions []*statstypes.AffiliateAttribution + + // Add taker affiliate attribution if present + if revSharesForFill.AffiliateRevShare != nil && + revSharesForFill.AffiliateRevShare.Recipient != "" && + bigFillQuoteQuantums.Sign() > 0 { + // Calculate the attributable volume based on the taker's current 30-day volume + // and the maximum attributable volume cap from affiliate parameters + takerAttributableVolume := k.getAttributableVolume( + ctx, + matchWithOrders.TakerOrder.GetSubaccountId().Owner, + bigFillQuoteQuantums.Uint64(), + affiliateParameters, + ) + if takerAttributableVolume > 0 { + affiliateRevenueAttributions = append(affiliateRevenueAttributions, &statstypes.AffiliateAttribution{ + Role: statstypes.AffiliateAttribution_ROLE_TAKER, + ReferrerAddress: revSharesForFill.AffiliateRevShare.Recipient, + ReferredVolumeQuoteQuantums: takerAttributableVolume, + }) + } + } + + // Add maker affiliate attribution if present + // Check if maker has an affiliate referrer + makerReferrer, makerHasReferrer := k.affiliatesKeeper.GetReferredBy( + ctx, + matchWithOrders.MakerOrder.GetSubaccountId().Owner, + ) + if makerHasReferrer && makerReferrer != "" && bigFillQuoteQuantums.Sign() > 0 { + // Calculate the attributable volume based on the maker's current 30-day volume + // and the maximum attributable volume cap from affiliate parameters + makerAttributableVolume := k.getAttributableVolume( + ctx, + matchWithOrders.MakerOrder.GetSubaccountId().Owner, + bigFillQuoteQuantums.Uint64(), + affiliateParameters, + ) + if makerAttributableVolume > 0 { + affiliateRevenueAttributions = append(affiliateRevenueAttributions, &statstypes.AffiliateAttribution{ + Role: statstypes.AffiliateAttribution_ROLE_MAKER, + ReferrerAddress: makerReferrer, + ReferredVolumeQuoteQuantums: makerAttributableVolume, + }) + } + } + + return affiliateRevenueAttributions +} + func (k Keeper) setOrderFillAmountsAndPruning( ctx sdk.Context, order types.Order, diff --git a/protocol/x/clob/keeper/process_single_match_affiliate_stats_test.go b/protocol/x/clob/keeper/process_single_match_affiliate_stats_test.go new file mode 100644 index 0000000000..14183504ba --- /dev/null +++ b/protocol/x/clob/keeper/process_single_match_affiliate_stats_test.go @@ -0,0 +1,890 @@ +package keeper_test + +import ( + "testing" + + "github.com/dydxprotocol/v4-chain/protocol/dtypes" + testapp "github.com/dydxprotocol/v4-chain/protocol/testutil/app" + "github.com/dydxprotocol/v4-chain/protocol/testutil/constants" + affiliatetypes "github.com/dydxprotocol/v4-chain/protocol/x/affiliates/types" + clobtypes "github.com/dydxprotocol/v4-chain/protocol/x/clob/types" + statstypes "github.com/dydxprotocol/v4-chain/protocol/x/stats/types" + satypes "github.com/dydxprotocol/v4-chain/protocol/x/subaccounts/types" + "github.com/stretchr/testify/require" +) + +// TestProcessSingleMatch_AffiliateAttribution_TakerOnly tests that when only the taker +// has an affiliate referrer, the attribution is correctly stored in BlockStats. +func TestProcessSingleMatch_AffiliateAttribution_TakerOnly(t *testing.T) { + tApp := testapp.NewTestAppBuilder(t).Build() + + ctx := tApp.InitChain() + k := tApp.App.ClobKeeper + + // Create taker and maker subaccounts with sufficient collateral + takerSubaccount := constants.Alice_Num0 + makerSubaccount := constants.Bob_Num0 + + tApp.App.SubaccountsKeeper.SetSubaccount(ctx, satypes.Subaccount{ + Id: &takerSubaccount, + AssetPositions: []*satypes.AssetPosition{ + { + AssetId: 0, + Quantums: dtypes.NewInt(1_000_000_000_000), // 1M USDC + }, + }, + }) + + tApp.App.SubaccountsKeeper.SetSubaccount(ctx, satypes.Subaccount{ + Id: &makerSubaccount, + AssetPositions: []*satypes.AssetPosition{ + { + AssetId: 0, + Quantums: dtypes.NewInt(1_000_000_000_000), // 1M USDC + }, + }, + }) + + // Set up affiliate tiers + err := tApp.App.AffiliatesKeeper.UpdateAffiliateTiers(ctx, affiliatetypes.AffiliateTiers{ + Tiers: []affiliatetypes.AffiliateTiers_Tier{ + { + ReqReferredVolumeQuoteQuantums: 0, + ReqStakedWholeCoins: 0, + TakerFeeSharePpm: 100_000, // 10% + }, + }, + }) + require.NoError(t, err) + + // Register taker with an affiliate referrer (use Carl as referrer) + referrerAddr := constants.CarlAccAddress.String() + err = tApp.App.AffiliatesKeeper.RegisterAffiliate(ctx, constants.Alice_Num0.Owner, referrerAddr) + require.NoError(t, err) + + // Create orders + takerOrder := clobtypes.Order{ + OrderId: clobtypes.OrderId{ + SubaccountId: takerSubaccount, + ClientId: 1, + OrderFlags: clobtypes.OrderIdFlags_ShortTerm, + ClobPairId: 0, + }, + Side: clobtypes.Order_SIDE_BUY, + Quantums: 100_000_000, // 1 BTC (in base quantums) + Subticks: 5_000_000_000, // $50,000 per BTC + GoodTilOneof: &clobtypes.Order_GoodTilBlock{GoodTilBlock: 100}, + } + + makerOrder := clobtypes.Order{ + OrderId: clobtypes.OrderId{ + SubaccountId: makerSubaccount, + ClientId: 1, + OrderFlags: clobtypes.OrderIdFlags_ShortTerm, + ClobPairId: 0, + }, + Side: clobtypes.Order_SIDE_SELL, + Quantums: 100_000_000, + Subticks: 5_000_000_000, + GoodTilOneof: &clobtypes.Order_GoodTilBlock{GoodTilBlock: 100}, + } + + matchWithOrders := &clobtypes.MatchWithOrders{ + TakerOrder: &takerOrder, + MakerOrder: &makerOrder, + FillAmount: satypes.BaseQuantums(100_000_000), // Fill full amount + } + + // Process the match + success, _, _, _, err := k.ProcessSingleMatch( + ctx, + matchWithOrders, + map[string]bool{}, + affiliatetypes.AffiliateParameters{ + Maximum_30DAttributableVolumePerReferredUserQuoteQuantums: 1_000_000_000_000, + }, + ) + require.NoError(t, err) + require.True(t, success) + + // Verify BlockStats contains the affiliate revenue attribution for taker only + blockStats := tApp.App.StatsKeeper.GetBlockStats(ctx) + require.Len(t, blockStats.Fills, 1) + + fill := blockStats.Fills[0] + require.Equal(t, constants.Alice_Num0.Owner, fill.Taker) + require.Equal(t, constants.Bob_Num0.Owner, fill.Maker) + + // Verify affiliate revenue attributions array + require.Len(t, fill.AffiliateAttributions, 1, "Should have exactly one attribution (taker only)") + + attribution := fill.AffiliateAttributions[0] + require.Equal(t, statstypes.AffiliateAttribution_ROLE_TAKER, attribution.Role) + require.Equal(t, referrerAddr, attribution.ReferrerAddress) +} + +// TestProcessSingleMatch_AffiliateAttribution_BothTakerAndMaker tests that when both +// taker and maker have affiliate referrers, both attributions are stored in BlockStats. +func TestProcessSingleMatch_AffiliateAttribution_BothTakerAndMaker(t *testing.T) { + tApp := testapp.NewTestAppBuilder(t).Build() + + ctx := tApp.InitChain() + k := tApp.App.ClobKeeper + + // Create subaccounts + takerSubaccount := constants.Alice_Num0 + makerSubaccount := constants.Bob_Num0 + + tApp.App.SubaccountsKeeper.SetSubaccount(ctx, satypes.Subaccount{ + Id: &takerSubaccount, + AssetPositions: []*satypes.AssetPosition{ + { + AssetId: 0, + Quantums: dtypes.NewInt(1_000_000_000_000), + }, + }, + }) + + tApp.App.SubaccountsKeeper.SetSubaccount(ctx, satypes.Subaccount{ + Id: &makerSubaccount, + AssetPositions: []*satypes.AssetPosition{ + { + AssetId: 0, + Quantums: dtypes.NewInt(1_000_000_000_000), + }, + }, + }) + + // Set up affiliate tiers + err := tApp.App.AffiliatesKeeper.UpdateAffiliateTiers(ctx, affiliatetypes.AffiliateTiers{ + Tiers: []affiliatetypes.AffiliateTiers_Tier{ + { + ReqReferredVolumeQuoteQuantums: 0, + ReqStakedWholeCoins: 0, + TakerFeeSharePpm: 100_000, // 10% + }, + }, + }) + require.NoError(t, err) + + // Register BOTH taker and maker with different affiliate referrers + takerReferrerAddr := constants.CarlAccAddress.String() + makerReferrerAddr := constants.DaveAccAddress.String() + + err = tApp.App.AffiliatesKeeper.RegisterAffiliate(ctx, constants.Alice_Num0.Owner, takerReferrerAddr) + require.NoError(t, err) + + err = tApp.App.AffiliatesKeeper.RegisterAffiliate(ctx, constants.Bob_Num0.Owner, makerReferrerAddr) + require.NoError(t, err) + + // Create orders + takerOrder := clobtypes.Order{ + OrderId: clobtypes.OrderId{ + SubaccountId: takerSubaccount, + ClientId: 1, + OrderFlags: clobtypes.OrderIdFlags_ShortTerm, + ClobPairId: 0, + }, + Side: clobtypes.Order_SIDE_BUY, + Quantums: 100_000_000, + Subticks: 5_000_000_000, + GoodTilOneof: &clobtypes.Order_GoodTilBlock{GoodTilBlock: 100}, + } + + makerOrder := clobtypes.Order{ + OrderId: clobtypes.OrderId{ + SubaccountId: makerSubaccount, + ClientId: 1, + OrderFlags: clobtypes.OrderIdFlags_ShortTerm, + ClobPairId: 0, + }, + Side: clobtypes.Order_SIDE_SELL, + Quantums: 100_000_000, + Subticks: 5_000_000_000, + GoodTilOneof: &clobtypes.Order_GoodTilBlock{GoodTilBlock: 100}, + } + + matchWithOrders := &clobtypes.MatchWithOrders{ + TakerOrder: &takerOrder, + MakerOrder: &makerOrder, + FillAmount: satypes.BaseQuantums(100_000_000), + } + + // Process the match + success, _, _, _, err := k.ProcessSingleMatch( + ctx, + matchWithOrders, + map[string]bool{}, + affiliatetypes.AffiliateParameters{ + Maximum_30DAttributableVolumePerReferredUserQuoteQuantums: 1_000_000_000_000, + }, + ) + require.NoError(t, err) + require.True(t, success) + + // Verify BlockStats contains BOTH affiliate revenue attributions + blockStats := tApp.App.StatsKeeper.GetBlockStats(ctx) + require.Len(t, blockStats.Fills, 1) + + fill := blockStats.Fills[0] + require.Equal(t, constants.Alice_Num0.Owner, fill.Taker) + require.Equal(t, constants.Bob_Num0.Owner, fill.Maker) + + // Verify we have TWO attributions + require.Len(t, fill.AffiliateAttributions, 2, "Should have two attributions (taker and maker)") + + // Find taker and maker attributions by role + var takerAttribution, makerAttribution *statstypes.AffiliateAttribution + for _, attr := range fill.AffiliateAttributions { + if attr.Role == statstypes.AffiliateAttribution_ROLE_TAKER { + takerAttribution = attr + } else if attr.Role == statstypes.AffiliateAttribution_ROLE_MAKER { + makerAttribution = attr + } + } + + require.NotNil(t, takerAttribution, "Should have taker attribution") + require.NotNil(t, makerAttribution, "Should have maker attribution") + + // Verify roles and referrers + require.Equal(t, statstypes.AffiliateAttribution_ROLE_TAKER, takerAttribution.Role) + require.Equal(t, takerReferrerAddr, takerAttribution.ReferrerAddress) + require.Equal(t, statstypes.AffiliateAttribution_ROLE_MAKER, makerAttribution.Role) + require.Equal(t, makerReferrerAddr, makerAttribution.ReferrerAddress) +} + +// TestProcessSingleMatch_AffiliateAttribution_NoReferrers tests that when neither +// taker nor maker has an affiliate referrer, no attributions are stored. +func TestProcessSingleMatch_AffiliateAttribution_NoReferrers(t *testing.T) { + tApp := testapp.NewTestAppBuilder(t).Build() + ctx := tApp.InitChain() + k := tApp.App.ClobKeeper + + // Create subaccounts (no affiliate registrations) + takerSubaccount := constants.Alice_Num0 + makerSubaccount := constants.Bob_Num0 + + tApp.App.SubaccountsKeeper.SetSubaccount(ctx, satypes.Subaccount{ + Id: &takerSubaccount, + AssetPositions: []*satypes.AssetPosition{ + { + AssetId: 0, + Quantums: dtypes.NewInt(1_000_000_000_000), + }, + }, + }) + + tApp.App.SubaccountsKeeper.SetSubaccount(ctx, satypes.Subaccount{ + Id: &makerSubaccount, + AssetPositions: []*satypes.AssetPosition{ + { + AssetId: 0, + Quantums: dtypes.NewInt(1_000_000_000_000), + }, + }, + }) + + // Create orders + takerOrder := clobtypes.Order{ + OrderId: clobtypes.OrderId{ + SubaccountId: takerSubaccount, + ClientId: 1, + OrderFlags: clobtypes.OrderIdFlags_ShortTerm, + ClobPairId: 0, + }, + Side: clobtypes.Order_SIDE_BUY, + Quantums: 100_000_000, + Subticks: 5_000_000_000, + GoodTilOneof: &clobtypes.Order_GoodTilBlock{GoodTilBlock: 100}, + } + + makerOrder := clobtypes.Order{ + OrderId: clobtypes.OrderId{ + SubaccountId: makerSubaccount, + ClientId: 1, + OrderFlags: clobtypes.OrderIdFlags_ShortTerm, + ClobPairId: 0, + }, + Side: clobtypes.Order_SIDE_SELL, + Quantums: 100_000_000, + Subticks: 5_000_000_000, + GoodTilOneof: &clobtypes.Order_GoodTilBlock{GoodTilBlock: 100}, + } + + matchWithOrders := &clobtypes.MatchWithOrders{ + TakerOrder: &takerOrder, + MakerOrder: &makerOrder, + FillAmount: satypes.BaseQuantums(100_000_000), + } + + // Process the match + success, _, _, _, err := k.ProcessSingleMatch( + ctx, + matchWithOrders, + map[string]bool{}, + affiliatetypes.AffiliateParameters{}, + ) + require.NoError(t, err) + require.True(t, success) + + // Verify BlockStats has no affiliate revenue attributions + blockStats := tApp.App.StatsKeeper.GetBlockStats(ctx) + require.Len(t, blockStats.Fills, 1) + + fill := blockStats.Fills[0] + require.Empty(t, fill.AffiliateAttributions, "Should have no attributions when neither has referrer") +} + +// TestProcessSingleMatch_AffiliateAttribution_VolumeCapApplied tests that the +// attributable volume cap is correctly applied when storing attributions. +func TestProcessSingleMatch_AffiliateAttribution_VolumeCapApplied(t *testing.T) { + lowCap := uint64(100_000_000_000) // Cap at 100k USDC + + tApp := testapp.NewTestAppBuilder(t).Build() + + ctx := tApp.InitChain() + k := tApp.App.ClobKeeper + + // Create subaccounts + takerSubaccount := constants.Alice_Num0 + makerSubaccount := constants.Bob_Num0 + + tApp.App.SubaccountsKeeper.SetSubaccount(ctx, satypes.Subaccount{ + Id: &takerSubaccount, + AssetPositions: []*satypes.AssetPosition{ + { + AssetId: 0, + Quantums: dtypes.NewInt(1_000_000_000_000), + }, + }, + }) + + tApp.App.SubaccountsKeeper.SetSubaccount(ctx, satypes.Subaccount{ + Id: &makerSubaccount, + AssetPositions: []*satypes.AssetPosition{ + { + AssetId: 0, + Quantums: dtypes.NewInt(1_000_000_000_000), + }, + }, + }) + + // Set up affiliate parameters with low cap + err := tApp.App.AffiliatesKeeper.UpdateAffiliateParameters(ctx, &affiliatetypes.MsgUpdateAffiliateParameters{ + Authority: constants.GovAuthority, + AffiliateParameters: affiliatetypes.AffiliateParameters{ + Maximum_30DAttributableVolumePerReferredUserQuoteQuantums: lowCap, + }, + }) + require.NoError(t, err) + + // Set up affiliate tiers + err = tApp.App.AffiliatesKeeper.UpdateAffiliateTiers(ctx, affiliatetypes.AffiliateTiers{ + Tiers: []affiliatetypes.AffiliateTiers_Tier{ + { + ReqReferredVolumeQuoteQuantums: 0, + ReqStakedWholeCoins: 0, + TakerFeeSharePpm: 100_000, // 10% + }, + }, + }) + require.NoError(t, err) + + // Register taker with referrer (use Carl as referrer) + referrerAddr := constants.CarlAccAddress.String() + err = tApp.App.AffiliatesKeeper.RegisterAffiliate(ctx, constants.Alice_Num0.Owner, referrerAddr) + require.NoError(t, err) + + // Create a large trade that exceeds the cap (400k USDC notional) + // Need to create a very large trade to exceed 100k cap + takerOrder := clobtypes.Order{ + OrderId: clobtypes.OrderId{ + SubaccountId: takerSubaccount, + ClientId: 1, + OrderFlags: clobtypes.OrderIdFlags_ShortTerm, + ClobPairId: 0, + }, + Side: clobtypes.Order_SIDE_BUY, + Quantums: 8_000_000_000, // 80 BTC to get large enough notional + Subticks: 5_000_000_000, // $50,000 per BTC + GoodTilOneof: &clobtypes.Order_GoodTilBlock{GoodTilBlock: 100}, + } + + makerOrder := clobtypes.Order{ + OrderId: clobtypes.OrderId{ + SubaccountId: makerSubaccount, + ClientId: 1, + OrderFlags: clobtypes.OrderIdFlags_ShortTerm, + ClobPairId: 0, + }, + Side: clobtypes.Order_SIDE_SELL, + Quantums: 8_000_000_000, + Subticks: 5_000_000_000, + GoodTilOneof: &clobtypes.Order_GoodTilBlock{GoodTilBlock: 100}, + } + + matchWithOrders := &clobtypes.MatchWithOrders{ + TakerOrder: &takerOrder, + MakerOrder: &makerOrder, + FillAmount: satypes.BaseQuantums(8_000_000_000), + } + + // Process the match + success, _, _, _, err := k.ProcessSingleMatch( + ctx, + matchWithOrders, + map[string]bool{}, + affiliatetypes.AffiliateParameters{ + Maximum_30DAttributableVolumePerReferredUserQuoteQuantums: lowCap, + }, + ) + require.NoError(t, err) + require.True(t, success) + + // Verify the attributed volume is capped + blockStats := tApp.App.StatsKeeper.GetBlockStats(ctx) + require.Len(t, blockStats.Fills, 1) + + fill := blockStats.Fills[0] + require.Len(t, fill.AffiliateAttributions, 1) + + attribution := fill.AffiliateAttributions[0] + require.Equal(t, statstypes.AffiliateAttribution_ROLE_TAKER, attribution.Role) + require.Equal(t, referrerAddr, attribution.ReferrerAddress) + + // Verify the trade notional exceeds the cap + require.Greater(t, fill.Notional, lowCap, "Trade notional should exceed the cap for this test") +} + +// TestProcessSingleMatch_AffiliateAttribution_AlreadyAtCap tests that when a user +// has already reached the 30-day attributable volume cap, no new volume is attributed. +func TestProcessSingleMatch_AffiliateAttribution_AlreadyAtCap(t *testing.T) { + cap := uint64(100_000_000_000) // 100k USDC cap + + tApp := testapp.NewTestAppBuilder(t).Build() + ctx := tApp.InitChain() + k := tApp.App.ClobKeeper + + // Create subaccounts + takerSubaccount := constants.Alice_Num0 + makerSubaccount := constants.Bob_Num0 + + tApp.App.SubaccountsKeeper.SetSubaccount(ctx, satypes.Subaccount{ + Id: &takerSubaccount, + AssetPositions: []*satypes.AssetPosition{ + { + AssetId: 0, + Quantums: dtypes.NewInt(1_000_000_000_000), + }, + }, + }) + + tApp.App.SubaccountsKeeper.SetSubaccount(ctx, satypes.Subaccount{ + Id: &makerSubaccount, + AssetPositions: []*satypes.AssetPosition{ + { + AssetId: 0, + Quantums: dtypes.NewInt(1_000_000_000_000), + }, + }, + }) + + // Set up affiliate parameters with cap + err := tApp.App.AffiliatesKeeper.UpdateAffiliateParameters(ctx, &affiliatetypes.MsgUpdateAffiliateParameters{ + Authority: constants.GovAuthority, + AffiliateParameters: affiliatetypes.AffiliateParameters{ + Maximum_30DAttributableVolumePerReferredUserQuoteQuantums: cap, + }, + }) + require.NoError(t, err) + + // Set up affiliate tiers + err = tApp.App.AffiliatesKeeper.UpdateAffiliateTiers(ctx, affiliatetypes.AffiliateTiers{ + Tiers: []affiliatetypes.AffiliateTiers_Tier{ + { + ReqReferredVolumeQuoteQuantums: 0, + ReqStakedWholeCoins: 0, + TakerFeeSharePpm: 100_000, // 10% + }, + }, + }) + require.NoError(t, err) + + // Register taker with referrer + referrerAddr := constants.CarlAccAddress.String() + err = tApp.App.AffiliatesKeeper.RegisterAffiliate(ctx, constants.Alice_Num0.Owner, referrerAddr) + require.NoError(t, err) + + // Set taker's previous ATTRIBUTED volume to EXACTLY the cap + // (This is the key fix - we track attributed volume, not total trading volume) + tApp.App.StatsKeeper.SetUserStats(ctx, constants.Alice_Num0.Owner, &statstypes.UserStats{ + TakerNotional: 200_000_000_000, // User has traded 200k total + MakerNotional: 0, + Affiliate_30DAttributedVolumeQuoteQuantums: cap, // But only 100k was attributed (at cap) + }) + + // Create a normal trade + takerOrder := clobtypes.Order{ + OrderId: clobtypes.OrderId{ + SubaccountId: takerSubaccount, + ClientId: 1, + OrderFlags: clobtypes.OrderIdFlags_ShortTerm, + ClobPairId: 0, + }, + Side: clobtypes.Order_SIDE_BUY, + Quantums: 100_000_000, // 1 BTC + Subticks: 5_000_000_000, // $50,000 per BTC + GoodTilOneof: &clobtypes.Order_GoodTilBlock{GoodTilBlock: 100}, + } + + makerOrder := clobtypes.Order{ + OrderId: clobtypes.OrderId{ + SubaccountId: makerSubaccount, + ClientId: 1, + OrderFlags: clobtypes.OrderIdFlags_ShortTerm, + ClobPairId: 0, + }, + Side: clobtypes.Order_SIDE_SELL, + Quantums: 100_000_000, + Subticks: 5_000_000_000, + GoodTilOneof: &clobtypes.Order_GoodTilBlock{GoodTilBlock: 100}, + } + + matchWithOrders := &clobtypes.MatchWithOrders{ + TakerOrder: &takerOrder, + MakerOrder: &makerOrder, + FillAmount: satypes.BaseQuantums(100_000_000), + } + + // Process the match + success, _, _, _, err := k.ProcessSingleMatch( + ctx, + matchWithOrders, + map[string]bool{}, + affiliatetypes.AffiliateParameters{ + Maximum_30DAttributableVolumePerReferredUserQuoteQuantums: cap, + }, + ) + require.NoError(t, err) + require.True(t, success) + + // Verify NO attribution was made since user is already at cap + blockStats := tApp.App.StatsKeeper.GetBlockStats(ctx) + require.Len(t, blockStats.Fills, 1) + + fill := blockStats.Fills[0] + + // Should have empty attributions array since no volume can be attributed + require.Empty(t, fill.AffiliateAttributions, + "Should have no attributions when referee is already at cap") +} + +// TestProcessSingleMatch_AffiliateAttribution_OverCap tests that when a user +// has volume EXCEEDING the 30-day cap, no new volume is attributed. +func TestProcessSingleMatch_AffiliateAttribution_OverCap(t *testing.T) { + cap := uint64(100_000_000_000) // 100k USDC cap + + tApp := testapp.NewTestAppBuilder(t).Build() + ctx := tApp.InitChain() + k := tApp.App.ClobKeeper + + // Create subaccounts + takerSubaccount := constants.Alice_Num0 + makerSubaccount := constants.Bob_Num0 + + tApp.App.SubaccountsKeeper.SetSubaccount(ctx, satypes.Subaccount{ + Id: &takerSubaccount, + AssetPositions: []*satypes.AssetPosition{ + { + AssetId: 0, + Quantums: dtypes.NewInt(1_000_000_000_000), + }, + }, + }) + + tApp.App.SubaccountsKeeper.SetSubaccount(ctx, satypes.Subaccount{ + Id: &makerSubaccount, + AssetPositions: []*satypes.AssetPosition{ + { + AssetId: 0, + Quantums: dtypes.NewInt(1_000_000_000_000), + }, + }, + }) + + // Set up affiliate parameters with cap + err := tApp.App.AffiliatesKeeper.UpdateAffiliateParameters(ctx, &affiliatetypes.MsgUpdateAffiliateParameters{ + Authority: constants.GovAuthority, + AffiliateParameters: affiliatetypes.AffiliateParameters{ + Maximum_30DAttributableVolumePerReferredUserQuoteQuantums: cap, + }, + }) + require.NoError(t, err) + + // Set up affiliate tiers + err = tApp.App.AffiliatesKeeper.UpdateAffiliateTiers(ctx, affiliatetypes.AffiliateTiers{ + Tiers: []affiliatetypes.AffiliateTiers_Tier{ + { + ReqReferredVolumeQuoteQuantums: 0, + ReqStakedWholeCoins: 0, + TakerFeeSharePpm: 100_000, // 10% + }, + }, + }) + require.NoError(t, err) + + // Register taker with referrer + referrerAddr := constants.CarlAccAddress.String() + err = tApp.App.AffiliatesKeeper.RegisterAffiliate(ctx, constants.Alice_Num0.Owner, referrerAddr) + require.NoError(t, err) + + // Set taker's previous ATTRIBUTED volume to EXCEED the cap + // (User has traded 300k total, but 150k was attributed, which exceeds 100k cap) + tApp.App.StatsKeeper.SetUserStats(ctx, constants.Alice_Num0.Owner, &statstypes.UserStats{ + TakerNotional: 200_000_000_000, // User traded 200k as taker + MakerNotional: 100_000_000_000, // User traded 100k as maker + Affiliate_30DAttributedVolumeQuoteQuantums: 150_000_000_000, // 150k attributed > 100k cap + }) + + // Create a normal trade + takerOrder := clobtypes.Order{ + OrderId: clobtypes.OrderId{ + SubaccountId: takerSubaccount, + ClientId: 1, + OrderFlags: clobtypes.OrderIdFlags_ShortTerm, + ClobPairId: 0, + }, + Side: clobtypes.Order_SIDE_BUY, + Quantums: 100_000_000, // 1 BTC + Subticks: 5_000_000_000, // $50,000 per BTC + GoodTilOneof: &clobtypes.Order_GoodTilBlock{GoodTilBlock: 100}, + } + + makerOrder := clobtypes.Order{ + OrderId: clobtypes.OrderId{ + SubaccountId: makerSubaccount, + ClientId: 1, + OrderFlags: clobtypes.OrderIdFlags_ShortTerm, + ClobPairId: 0, + }, + Side: clobtypes.Order_SIDE_SELL, + Quantums: 100_000_000, + Subticks: 5_000_000_000, + GoodTilOneof: &clobtypes.Order_GoodTilBlock{GoodTilBlock: 100}, + } + + matchWithOrders := &clobtypes.MatchWithOrders{ + TakerOrder: &takerOrder, + MakerOrder: &makerOrder, + FillAmount: satypes.BaseQuantums(100_000_000), + } + + // Process the match + success, _, _, _, err := k.ProcessSingleMatch( + ctx, + matchWithOrders, + map[string]bool{}, + affiliatetypes.AffiliateParameters{ + Maximum_30DAttributableVolumePerReferredUserQuoteQuantums: cap, + }, + ) + require.NoError(t, err) + require.True(t, success) + + // Verify NO attribution was made since user exceeds cap + blockStats := tApp.App.StatsKeeper.GetBlockStats(ctx) + require.Len(t, blockStats.Fills, 1) + + fill := blockStats.Fills[0] + + // Should have empty attributions array since user is over cap + require.Empty(t, fill.AffiliateAttributions, + "Should have no attributions when referee exceeds cap") +} + +// TestProcessSingleMatch_AffiliateAttribution_CapWithExpiration tests that when +// old stats expire from the 30-day window, a user who was over the cap can start +// receiving attributions again. +func TestProcessSingleMatch_AffiliateAttribution_CapWithExpiration(t *testing.T) { + cap := uint64(100_000_000_000) // 100k USDC cap + + tApp := testapp.NewTestAppBuilder(t).Build() + ctx := tApp.InitChain() + k := tApp.App.ClobKeeper + + // Create subaccounts + takerSubaccount := constants.Alice_Num0 + makerSubaccount := constants.Bob_Num0 + + tApp.App.SubaccountsKeeper.SetSubaccount(ctx, satypes.Subaccount{ + Id: &takerSubaccount, + AssetPositions: []*satypes.AssetPosition{ + { + AssetId: 0, + Quantums: dtypes.NewInt(1_000_000_000_000), + }, + }, + }) + + tApp.App.SubaccountsKeeper.SetSubaccount(ctx, satypes.Subaccount{ + Id: &makerSubaccount, + AssetPositions: []*satypes.AssetPosition{ + { + AssetId: 0, + Quantums: dtypes.NewInt(1_000_000_000_000), + }, + }, + }) + + // Set up affiliate parameters with cap + err := tApp.App.AffiliatesKeeper.UpdateAffiliateParameters(ctx, &affiliatetypes.MsgUpdateAffiliateParameters{ + Authority: constants.GovAuthority, + AffiliateParameters: affiliatetypes.AffiliateParameters{ + Maximum_30DAttributableVolumePerReferredUserQuoteQuantums: cap, + }, + }) + require.NoError(t, err) + + // Set up affiliate tiers + err = tApp.App.AffiliatesKeeper.UpdateAffiliateTiers(ctx, affiliatetypes.AffiliateTiers{ + Tiers: []affiliatetypes.AffiliateTiers_Tier{ + { + ReqReferredVolumeQuoteQuantums: 0, + ReqStakedWholeCoins: 0, + TakerFeeSharePpm: 100_000, // 10% + }, + }, + }) + require.NoError(t, err) + + // Register taker with referrer + referrerAddr := constants.CarlAccAddress.String() + err = tApp.App.AffiliatesKeeper.RegisterAffiliate(ctx, constants.Alice_Num0.Owner, referrerAddr) + require.NoError(t, err) + + // SCENARIO 1: User is over cap (150k attributed volume) + tApp.App.StatsKeeper.SetUserStats(ctx, constants.Alice_Num0.Owner, &statstypes.UserStats{ + TakerNotional: 200_000_000_000, // User traded 200k + MakerNotional: 100_000_000_000, // User traded 100k + Affiliate_30DAttributedVolumeQuoteQuantums: 150_000_000_000, // 150k attributed > 100k cap + }) + + // Create first trade + takerOrder1 := clobtypes.Order{ + OrderId: clobtypes.OrderId{ + SubaccountId: takerSubaccount, + ClientId: 1, + OrderFlags: clobtypes.OrderIdFlags_ShortTerm, + ClobPairId: 0, + }, + Side: clobtypes.Order_SIDE_BUY, + Quantums: 100_000_000, // 1 BTC = ~5k USDC + Subticks: 5_000_000_000, // $50,000 per BTC + GoodTilOneof: &clobtypes.Order_GoodTilBlock{GoodTilBlock: 100}, + } + + makerOrder1 := clobtypes.Order{ + OrderId: clobtypes.OrderId{ + SubaccountId: makerSubaccount, + ClientId: 1, + OrderFlags: clobtypes.OrderIdFlags_ShortTerm, + ClobPairId: 0, + }, + Side: clobtypes.Order_SIDE_SELL, + Quantums: 100_000_000, + Subticks: 5_000_000_000, + GoodTilOneof: &clobtypes.Order_GoodTilBlock{GoodTilBlock: 100}, + } + + matchWithOrders1 := &clobtypes.MatchWithOrders{ + TakerOrder: &takerOrder1, + MakerOrder: &makerOrder1, + FillAmount: satypes.BaseQuantums(100_000_000), + } + + // Process first match - should have NO attribution + success, _, _, _, err := k.ProcessSingleMatch( + ctx, + matchWithOrders1, + map[string]bool{}, + affiliatetypes.AffiliateParameters{ + Maximum_30DAttributableVolumePerReferredUserQuoteQuantums: cap, + }, + ) + require.NoError(t, err) + require.True(t, success) + + blockStats := tApp.App.StatsKeeper.GetBlockStats(ctx) + require.Len(t, blockStats.Fills, 1) + require.Empty(t, blockStats.Fills[0].AffiliateAttributions, + "First trade: No attribution since user is over cap") + + // SCENARIO 2: Simulate expiration - old attributed volume expires from 30-day window + // Now user only has 80k attributed volume, which is BELOW the 100k cap + tApp.App.StatsKeeper.SetUserStats(ctx, constants.Alice_Num0.Owner, &statstypes.UserStats{ + TakerNotional: 150_000_000_000, // Still trading (down from 200k as old trades expired) + MakerNotional: 70_000_000_000, // Still trading (down from 100k) + Affiliate_30DAttributedVolumeQuoteQuantums: 80_000_000_000, // 80k attributed (down from + // 150k - old attributions expired) + // 80k < 100k cap, so 20k capacity available + }) + + // Clear block stats for second trade + tApp.App.StatsKeeper.SetBlockStats(ctx, &statstypes.BlockStats{}) + + // Create second trade + takerOrder2 := clobtypes.Order{ + OrderId: clobtypes.OrderId{ + SubaccountId: takerSubaccount, + ClientId: 2, // Different client ID + OrderFlags: clobtypes.OrderIdFlags_ShortTerm, + ClobPairId: 0, + }, + Side: clobtypes.Order_SIDE_BUY, + Quantums: 100_000_000, // 1 BTC = ~5k USDC + Subticks: 5_000_000_000, // $50,000 per BTC + GoodTilOneof: &clobtypes.Order_GoodTilBlock{GoodTilBlock: 100}, + } + + makerOrder2 := clobtypes.Order{ + OrderId: clobtypes.OrderId{ + SubaccountId: makerSubaccount, + ClientId: 2, + OrderFlags: clobtypes.OrderIdFlags_ShortTerm, + ClobPairId: 0, + }, + Side: clobtypes.Order_SIDE_SELL, + Quantums: 100_000_000, + Subticks: 5_000_000_000, + GoodTilOneof: &clobtypes.Order_GoodTilBlock{GoodTilBlock: 100}, + } + + matchWithOrders2 := &clobtypes.MatchWithOrders{ + TakerOrder: &takerOrder2, + MakerOrder: &makerOrder2, + FillAmount: satypes.BaseQuantums(100_000_000), + } + + // Process second match - should NOW have attribution since user is below cap + success, _, _, _, err = k.ProcessSingleMatch( + ctx, + matchWithOrders2, + map[string]bool{}, + affiliatetypes.AffiliateParameters{ + Maximum_30DAttributableVolumePerReferredUserQuoteQuantums: cap, + }, + ) + require.NoError(t, err) + require.True(t, success) + + blockStats = tApp.App.StatsKeeper.GetBlockStats(ctx) + require.Len(t, blockStats.Fills, 1) + + fill2 := blockStats.Fills[0] + require.Len(t, fill2.AffiliateAttributions, 1, + "Second trade: Should have attribution after old stats expired") + + attribution := fill2.AffiliateAttributions[0] + require.Equal(t, statstypes.AffiliateAttribution_ROLE_TAKER, attribution.Role) + require.Equal(t, referrerAddr, attribution.ReferrerAddress) +} diff --git a/protocol/x/clob/types/expected_keepers.go b/protocol/x/clob/types/expected_keepers.go index cd7bcef9b4..84bfd08206 100644 --- a/protocol/x/clob/types/expected_keepers.go +++ b/protocol/x/clob/types/expected_keepers.go @@ -183,8 +183,14 @@ type PricesKeeper interface { } type StatsKeeper interface { - RecordFill(ctx sdk.Context, takerAddress string, makerAddress string, - notional *big.Int, affiliateFeeGenerated *big.Int) + RecordFill( + ctx sdk.Context, + takerAddress string, + makerAddress string, + notional *big.Int, + affiliateFeeGenerated *big.Int, + affiliateAttributions []*stattypes.AffiliateAttribution, + ) GetUserStats(ctx sdk.Context, address string) *stattypes.UserStats } @@ -224,6 +230,7 @@ type AffiliatesKeeper interface { GetAffiliateWhitelistMap(ctx sdk.Context) (map[string]uint32, error) GetAffiliateOverridesMap(ctx sdk.Context) (map[string]bool, error) GetAffiliateParameters(ctx sdk.Context) (affiliatetypes.AffiliateParameters, error) + GetReferredBy(ctx sdk.Context, referee string) (referrer string, found bool) } type AccountPlusKeeper interface { diff --git a/protocol/x/revshare/keeper/revshare.go b/protocol/x/revshare/keeper/revshare.go index 5b1ef5b191..4fba70e74a 100644 --- a/protocol/x/revshare/keeper/revshare.go +++ b/protocol/x/revshare/keeper/revshare.go @@ -327,6 +327,20 @@ func (k Keeper) getAffiliateRevShares( return nil, big.NewInt(0), nil } feesShared := lib.BigMulPpm(takerFee, lib.BigU(feeSharePpm), false) + + // Cap the affiliate revenue share if it exceeds the maximum 30d affiliate revenue per referred user + if userStats != nil { + cap := affiliateParams.Maximum_30DAffiliateRevenuePerReferredUserQuoteQuantums + if cap != 0 { + revenueGenerated := userStats.Affiliate_30DRevenueGeneratedQuantums + // We know revenueGenerated < cap here because of the check above + maxFee := new(big.Int).SetUint64(cap - revenueGenerated) + if feesShared.Cmp(maxFee) > 0 { + feesShared = maxFee + } + } + } + return []types.RevShare{ { Recipient: takerAffiliateAddr, diff --git a/protocol/x/revshare/keeper/revshare_test.go b/protocol/x/revshare/keeper/revshare_test.go index 0ff9e43280..43f2472d6b 100644 --- a/protocol/x/revshare/keeper/revshare_test.go +++ b/protocol/x/revshare/keeper/revshare_test.go @@ -421,6 +421,66 @@ func TestKeeper_GetAllRevShares_Valid(t *testing.T) { require.NoError(t, err) }, }, + { + name: "Affiliates has about to go over the edge", + expectedRevSharesForFill: types.RevSharesForFill{ + AllRevShares: []types.RevShare{ + { + Recipient: constants.BobAccAddress.String(), + RevShareFeeSource: types.REV_SHARE_FEE_SOURCE_TAKER_FEE, + RevShareType: types.REV_SHARE_TYPE_AFFILIATE, + QuoteQuantums: big.NewInt(50_000), + RevSharePpm: 50_000, + }, + }, + AffiliateRevShare: &types.RevShare{ + Recipient: constants.BobAccAddress.String(), + RevShareFeeSource: types.REV_SHARE_FEE_SOURCE_TAKER_FEE, + RevShareType: types.REV_SHARE_TYPE_AFFILIATE, + QuoteQuantums: big.NewInt(50_000), + RevSharePpm: 50_000, + }, + FeeSourceToQuoteQuantums: map[types.RevShareFeeSource]*big.Int{ + types.REV_SHARE_FEE_SOURCE_TAKER_FEE: big.NewInt(50_000), // affiliate rev share fees + // unconditional + market mapper rev shares fees + types.REV_SHARE_FEE_SOURCE_NET_PROTOCOL_REVENUE: big.NewInt(0), + types.REV_SHARE_FEE_SOURCE_MAKER_FEE: big.NewInt(0), + }, + FeeSourceToRevSharePpm: map[types.RevShareFeeSource]uint32{ + types.REV_SHARE_FEE_SOURCE_TAKER_FEE: 50_000, // affiliate rev share fee ppm + types.REV_SHARE_FEE_SOURCE_NET_PROTOCOL_REVENUE: 0, // unconditional + market mapper rev share fee ppm + types.REV_SHARE_FEE_SOURCE_MAKER_FEE: 0, + }, + }, + fill: clobtypes.FillForProcess{ + TakerAddr: constants.AliceAccAddress.String(), + TakerFeeQuoteQuantums: big.NewInt(10_000_000), + MakerAddr: constants.BobAccAddress.String(), + MakerFeeQuoteQuantums: big.NewInt(2_000_000), + FillQuoteQuantums: big.NewInt(100_000_000_000), + ProductId: perpetualId, + MonthlyRollingTakerVolumeQuantums: 1_000_000_000_000, + MarketId: marketId, + }, + setup: func(tApp *testapp.TestApp, ctx sdk.Context, keeper *keeper.Keeper, + affiliatesKeeper *affiliateskeeper.Keeper, statsKeeper *statsKeeper.Keeper) { + require.NoError(t, affiliatesKeeper.UpdateAffiliateParameters(ctx, &affiliatetypes.MsgUpdateAffiliateParameters{ + AffiliateParameters: affiliatetypes.AffiliateParameters{ + Maximum_30DAffiliateRevenuePerReferredUserQuoteQuantums: 1_000_000, + }, + })) + + statsKeeper.SetUserStats(ctx, constants.AliceAccAddress.String(), &statstypes.UserStats{ + Affiliate_30DRevenueGeneratedQuantums: 950_000, + }) + + err := affiliatesKeeper.UpdateAffiliateTiers(ctx, affiliatetypes.DefaultAffiliateTiers) + require.NoError(t, err) + err = affiliatesKeeper.RegisterAffiliate(ctx, constants.AliceAccAddress.String(), + constants.BobAccAddress.String()) + require.NoError(t, err) + }, + }, { name: "Valid rev-share from affiliates, negative maker fee and unconditional and market mapper", expectedRevSharesForFill: types.RevSharesForFill{ @@ -1376,16 +1436,16 @@ func TestKeeper_GetAllRevShares_Valid(t *testing.T) { keeper := tApp.App.RevShareKeeper affiliatesKeeper := tApp.App.AffiliatesKeeper statsKeeper := tApp.App.StatsKeeper - if tc.setup != nil { - tc.setup(tApp, ctx, &keeper, &affiliatesKeeper, &statsKeeper) - } - require.NoError(t, affiliatesKeeper.UpdateAffiliateParameters(ctx, &affiliatetypes.MsgUpdateAffiliateParameters{ AffiliateParameters: affiliatetypes.AffiliateParameters{ Maximum_30DAffiliateRevenuePerReferredUserQuoteQuantums: 1_000_000_000_000, }, })) + if tc.setup != nil { + tc.setup(tApp, ctx, &keeper, &affiliatesKeeper, &statsKeeper) + } + keeper.CreateNewMarketRevShare(ctx, marketId) affiliateOverridesMap, err := affiliatesKeeper.GetAffiliateOverridesMap(ctx) require.NoError(t, err) diff --git a/protocol/x/stats/keeper/grpc_query_test.go b/protocol/x/stats/keeper/grpc_query_test.go index 3f7852d1da..ba294ac949 100644 --- a/protocol/x/stats/keeper/grpc_query_test.go +++ b/protocol/x/stats/keeper/grpc_query_test.go @@ -130,8 +130,11 @@ func TestUserStats(t *testing.T) { k := tApp.App.StatsKeeper user := "alice" userStats := &types.UserStats{ - TakerNotional: 10, - MakerNotional: 10, + TakerNotional: 10, + MakerNotional: 10, + Affiliate_30DRevenueGeneratedQuantums: 100, + Affiliate_30DReferredVolumeQuoteQuantums: 500, + Affiliate_30DAttributedVolumeQuoteQuantums: 250, } k.SetUserStats(ctx, user, userStats) @@ -149,6 +152,21 @@ func TestUserStats(t *testing.T) { }, err: nil, }, + "Success with attributed volume": { + req: &types.QueryUserStatsRequest{ + User: user, + }, + res: &types.QueryUserStatsResponse{ + Stats: &types.UserStats{ + TakerNotional: 10, + MakerNotional: 10, + Affiliate_30DRevenueGeneratedQuantums: 100, + Affiliate_30DReferredVolumeQuoteQuantums: 500, + Affiliate_30DAttributedVolumeQuoteQuantums: 250, + }, + }, + err: nil, + }, "Nil": { req: nil, res: nil, @@ -162,6 +180,15 @@ func TestUserStats(t *testing.T) { } else { require.NoError(t, err) require.Equal(t, tc.res, res) + // Explicitly verify attributed volume field is present + if tc.res != nil && tc.res.Stats != nil { + require.Equal( + t, + tc.res.Stats.Affiliate_30DAttributedVolumeQuoteQuantums, + res.Stats.Affiliate_30DAttributedVolumeQuoteQuantums, + "Attributed volume should be included in response", + ) + } } }) } diff --git a/protocol/x/stats/keeper/keeper.go b/protocol/x/stats/keeper/keeper.go index a99efb8411..e55c3033cc 100644 --- a/protocol/x/stats/keeper/keeper.go +++ b/protocol/x/stats/keeper/keeper.go @@ -85,6 +85,7 @@ func (k Keeper) RecordFill( makerAddress string, notional *big.Int, affiliateFeeGenerated *big.Int, + affiliateAttributions []*types.AffiliateAttribution, ) { blockStats := k.GetBlockStats(ctx) blockStats.Fills = append( @@ -94,6 +95,7 @@ func (k Keeper) RecordFill( Maker: makerAddress, Notional: notional.Uint64(), AffiliateFeeGeneratedQuantums: affiliateFeeGenerated.Uint64(), + AffiliateAttributions: affiliateAttributions, }, ) k.SetBlockStats(ctx, blockStats) @@ -231,6 +233,53 @@ func (k Keeper) ProcessBlockStats(ctx sdk.Context) { } userStatsMap[fill.Taker].Stats.TakerNotional += fill.Notional userStatsMap[fill.Maker].Stats.MakerNotional += fill.Notional + + // Track affiliate revenue attributions if present (can include both taker and maker) + for _, attribution := range fill.AffiliateAttributions { + if attribution != nil { + referrer := attribution.ReferrerAddress + + // Determine referee address based on role + var referee string + if attribution.Role == types.AffiliateAttribution_ROLE_TAKER { + referee = fill.Taker + } else if attribution.Role == types.AffiliateAttribution_ROLE_MAKER { + referee = fill.Maker + } else { + ctx.Logger().Error("invalid affiliate attribution role. Skipping", "role", attribution.Role) + continue + } + + // Initialize referrer stats if needed + if _, ok := userStatsMap[referrer]; !ok { + userStatsMap[referrer] = &types.EpochStats_UserWithStats{ + User: referrer, + Stats: &types.UserStats{}, + } + } + // Track affiliate referred volume for the referrer in this epoch snapshot + userStatsMap[referrer].Stats.Affiliate_30DReferredVolumeQuoteQuantums += attribution.ReferredVolumeQuoteQuantums + // Track affiliate referred volume for the referrer in UserStats + referrerUserStats := k.GetUserStats(ctx, referrer) + referrerUserStats.Affiliate_30DReferredVolumeQuoteQuantums += attribution.ReferredVolumeQuoteQuantums + k.SetUserStats(ctx, referrer, referrerUserStats) + + // Initialize referee stats if needed + if _, ok := userStatsMap[referee]; !ok { + userStatsMap[referee] = &types.EpochStats_UserWithStats{ + User: referee, + Stats: &types.UserStats{}, + } + } + // Track attributed volume for the referee (the trader whose volume was attributed) + userStatsMap[referee].Stats.Affiliate_30DAttributedVolumeQuoteQuantums += attribution.ReferredVolumeQuoteQuantums + // Track attributed volume for the referee in UserStats + refereeUserStats := k.GetUserStats(ctx, referee) + refereeUserStats.Affiliate_30DAttributedVolumeQuoteQuantums += attribution.ReferredVolumeQuoteQuantums + k.SetUserStats(ctx, referee, refereeUserStats) + } + } + // Track affiliate revenue generated on the taker in this epoch snapshot userStatsMap[fill.Taker].Stats.Affiliate_30DRevenueGeneratedQuantums += fill.AffiliateFeeGeneratedQuantums @@ -283,6 +332,8 @@ func (k Keeper) ExpireOldStats(ctx sdk.Context) { stats.TakerNotional -= removedStats.Stats.TakerNotional stats.MakerNotional -= removedStats.Stats.MakerNotional stats.Affiliate_30DRevenueGeneratedQuantums -= removedStats.Stats.Affiliate_30DRevenueGeneratedQuantums + stats.Affiliate_30DReferredVolumeQuoteQuantums -= removedStats.Stats.Affiliate_30DReferredVolumeQuoteQuantums + stats.Affiliate_30DAttributedVolumeQuoteQuantums -= removedStats.Stats.Affiliate_30DAttributedVolumeQuoteQuantums k.SetUserStats(ctx, removedStats.User, stats) // Just remove TakerNotional to avoid double counting diff --git a/protocol/x/stats/keeper/keeper_test.go b/protocol/x/stats/keeper/keeper_test.go index 3b8b55af94..a5477c9431 100644 --- a/protocol/x/stats/keeper/keeper_test.go +++ b/protocol/x/stats/keeper/keeper_test.go @@ -26,10 +26,11 @@ func TestLogger(t *testing.T) { } type recordFillArgs struct { - taker string - maker string - notional *big.Int - affiliateFee *big.Int + taker string + maker string + notional *big.Int + affiliateFee *big.Int + affiliateAttributions []*types.AffiliateAttribution } func TestRecordFill(t *testing.T) { @@ -45,34 +46,94 @@ func TestRecordFill(t *testing.T) { }, "single fill": { []recordFillArgs{ - {"taker", "maker", new(big.Int).SetUint64(123), big.NewInt(0)}, + { + taker: "taker", + maker: "maker", + notional: new(big.Int).SetUint64(123), + affiliateFee: big.NewInt(0), + affiliateAttributions: []*types.AffiliateAttribution{ + { + Role: types.AffiliateAttribution_ROLE_TAKER, + ReferrerAddress: "referrer", + ReferredVolumeQuoteQuantums: 123, + }, + }, + }, }, &types.BlockStats{ Fills: []*types.BlockStats_Fill{ { - Taker: "taker", - Maker: "maker", - Notional: 123, + Taker: "taker", + Maker: "maker", + Notional: 123, + AffiliateFeeGeneratedQuantums: 0, + AffiliateAttributions: []*types.AffiliateAttribution{ + { + Role: types.AffiliateAttribution_ROLE_TAKER, + ReferrerAddress: "referrer", + ReferredVolumeQuoteQuantums: 123, + }, + }, }, }, }, }, "multiple fills": { []recordFillArgs{ - {"alice", "bob", new(big.Int).SetUint64(123), big.NewInt(0)}, - {"bob", "alice", new(big.Int).SetUint64(321), big.NewInt(0)}, + { + taker: "alice", + maker: "bob", + notional: new(big.Int).SetUint64(123), + affiliateFee: big.NewInt(0), + affiliateAttributions: []*types.AffiliateAttribution{ + { + Role: types.AffiliateAttribution_ROLE_TAKER, + ReferrerAddress: "referrer", + ReferredVolumeQuoteQuantums: 123, + }, + }, + }, + { + taker: "bob", + maker: "alice", + notional: new(big.Int).SetUint64(321), + affiliateFee: big.NewInt(0), + affiliateAttributions: []*types.AffiliateAttribution{ + { + Role: types.AffiliateAttribution_ROLE_TAKER, + ReferrerAddress: "referrer", + ReferredVolumeQuoteQuantums: 321, + }, + }, + }, }, &types.BlockStats{ Fills: []*types.BlockStats_Fill{ { - Taker: "alice", - Maker: "bob", - Notional: 123, + Taker: "alice", + Maker: "bob", + Notional: 123, + AffiliateFeeGeneratedQuantums: 0, + AffiliateAttributions: []*types.AffiliateAttribution{ + { + Role: types.AffiliateAttribution_ROLE_TAKER, + ReferrerAddress: "referrer", + ReferredVolumeQuoteQuantums: 123, + }, + }, }, { - Taker: "bob", - Maker: "alice", - Notional: 321, + Taker: "bob", + Maker: "alice", + Notional: 321, + AffiliateFeeGeneratedQuantums: 0, + AffiliateAttributions: []*types.AffiliateAttribution{ + { + Role: types.AffiliateAttribution_ROLE_TAKER, + ReferrerAddress: "referrer", + ReferredVolumeQuoteQuantums: 321, + }, + }, }, }, }, @@ -86,7 +147,7 @@ func TestRecordFill(t *testing.T) { k := tApp.App.StatsKeeper for _, fill := range tc.args { - k.RecordFill(ctx, fill.taker, fill.maker, fill.notional, fill.affiliateFee) + k.RecordFill(ctx, fill.taker, fill.maker, fill.notional, fill.affiliateFee, fill.affiliateAttributions) } require.Equal(t, tc.expectedBlockStats, k.GetBlockStats(ctx)) }) @@ -192,6 +253,228 @@ func TestProcessBlockStats(t *testing.T) { }, }, }, k.GetEpochStatsOrNil(ctx, 1)) + + // Test affiliate revenue attribution + k.SetBlockStats(ctx, &types.BlockStats{ + Fills: []*types.BlockStats_Fill{ + { + Taker: "taker", + Maker: "maker", + Notional: 100_000_000_000, + AffiliateFeeGeneratedQuantums: 50_000_000, + AffiliateAttributions: []*types.AffiliateAttribution{ + { + Role: types.AffiliateAttribution_ROLE_TAKER, + ReferrerAddress: "referrer", + ReferredVolumeQuoteQuantums: 100_000_000_000, + }, + }, + }, + }, + }) + k.ProcessBlockStats(ctx) + + // Verify referrer's UserStats has the referred volume + assert.Equal(t, &types.UserStats{ + Affiliate_30DReferredVolumeQuoteQuantums: 100_000_000_000, + }, k.GetUserStats(ctx, "referrer")) + + // Verify taker has the affiliate fee generated AND attributed volume + assert.Equal(t, &types.UserStats{ + TakerNotional: 100_000_000_000, + Affiliate_30DRevenueGeneratedQuantums: 50_000_000, + Affiliate_30DAttributedVolumeQuoteQuantums: 100_000_000_000, // Taker's volume was attributed + }, k.GetUserStats(ctx, "taker")) + + // Verify maker stats + assert.Equal(t, &types.UserStats{ + MakerNotional: 100_000_000_000, + }, k.GetUserStats(ctx, "maker")) + + // Verify global stats includes the new fill + assert.Equal(t, &types.GlobalStats{ + NotionalTraded: 100_000_000_025, + }, k.GetGlobalStats(ctx)) + + // Verify referrer is in epoch stats with correct referred volume + epochStats := k.GetEpochStatsOrNil(ctx, 1) + require.NotNil(t, epochStats) + var referrerFound bool + for _, userStats := range epochStats.Stats { + if userStats.User == "referrer" { + referrerFound = true + assert.Equal(t, uint64(100_000_000_000), userStats.Stats.Affiliate_30DReferredVolumeQuoteQuantums) + break + } + } + require.True(t, referrerFound, "referrer should be in epoch stats") + + // Test multiple fills with same referrer - referred volume should accumulate + k.SetBlockStats(ctx, &types.BlockStats{ + Fills: []*types.BlockStats_Fill{ + { + Taker: "taker2", + Maker: "maker2", + Notional: 50_000_000_000, + AffiliateFeeGeneratedQuantums: 25_000_000, + AffiliateAttributions: []*types.AffiliateAttribution{ + { + Role: types.AffiliateAttribution_ROLE_TAKER, + ReferrerAddress: "referrer", + ReferredVolumeQuoteQuantums: 50_000_000_000, + }, + }, + }, + }, + }) + k.ProcessBlockStats(ctx) + + // Verify referrer's referred volume accumulated + assert.Equal(t, &types.UserStats{ + Affiliate_30DReferredVolumeQuoteQuantums: 150_000_000_000, + }, k.GetUserStats(ctx, "referrer")) + + // Verify referrer's epoch stats accumulated + epochStats = k.GetEpochStatsOrNil(ctx, 1) + require.NotNil(t, epochStats) + referrerFound = false + for _, userStats := range epochStats.Stats { + if userStats.User == "referrer" { + referrerFound = true + assert.Equal(t, uint64(150_000_000_000), userStats.Stats.Affiliate_30DReferredVolumeQuoteQuantums) + break + } + } + require.True(t, referrerFound, "referrer should be in epoch stats") + + // Test fill with capped attributable volume + k.SetBlockStats(ctx, &types.BlockStats{ + Fills: []*types.BlockStats_Fill{ + { + Taker: "taker3", + Maker: "maker3", + Notional: 100_000_000_000, + AffiliateFeeGeneratedQuantums: 50_000_000, + AffiliateAttributions: []*types.AffiliateAttribution{ + { + Role: types.AffiliateAttribution_ROLE_TAKER, + ReferrerAddress: "referrer2", + ReferredVolumeQuoteQuantums: 30_000_000_000, + }, + }, + }, + }, + }) + k.ProcessBlockStats(ctx) + + // Verify referrer2's referred volume reflects the capped amount + assert.Equal(t, &types.UserStats{ + Affiliate_30DReferredVolumeQuoteQuantums: 30_000_000_000, + }, k.GetUserStats(ctx, "referrer2")) + + // Verify referrer2's epoch stats has the capped amount + epochStats = k.GetEpochStatsOrNil(ctx, 1) + require.NotNil(t, epochStats) + var referrer2Found bool + for _, userStats := range epochStats.Stats { + if userStats.User == "referrer2" { + referrer2Found = true + assert.Equal(t, uint64(30_000_000_000), userStats.Stats.Affiliate_30DReferredVolumeQuoteQuantums) + break + } + } + require.True(t, referrer2Found, "referrer2 should be in epoch stats") + + // Test fill without affiliate revenue attribution - should not affect referrer stats + k.SetBlockStats(ctx, &types.BlockStats{ + Fills: []*types.BlockStats_Fill{ + { + Taker: "taker4", + Maker: "maker4", + Notional: 50_000_000_000, + AffiliateFeeGeneratedQuantums: 0, + AffiliateAttributions: nil, + }, + }, + }) + k.ProcessBlockStats(ctx) + + // Verify referrer stats unchanged + assert.Equal(t, &types.UserStats{ + Affiliate_30DReferredVolumeQuoteQuantums: 150_000_000_000, + }, k.GetUserStats(ctx, "referrer")) + + // Verify referrer2 stats unchanged + assert.Equal(t, &types.UserStats{ + Affiliate_30DReferredVolumeQuoteQuantums: 30_000_000_000, + }, k.GetUserStats(ctx, "referrer2")) + + // Test fill where both taker AND maker have affiliate attributions + k.SetBlockStats(ctx, &types.BlockStats{ + Fills: []*types.BlockStats_Fill{ + { + Taker: "taker5", + Maker: "maker5", + Notional: 80_000_000_000, + AffiliateFeeGeneratedQuantums: 40_000_000, + AffiliateAttributions: []*types.AffiliateAttribution{ + { + Role: types.AffiliateAttribution_ROLE_TAKER, + ReferrerAddress: "referrer_for_taker", + ReferredVolumeQuoteQuantums: 80_000_000_000, + }, + { + Role: types.AffiliateAttribution_ROLE_MAKER, + ReferrerAddress: "referrer_for_maker", + ReferredVolumeQuoteQuantums: 80_000_000_000, + }, + }, + }, + }, + }) + k.ProcessBlockStats(ctx) + + // Verify taker's referrer received the attributed volume + assert.Equal(t, &types.UserStats{ + Affiliate_30DReferredVolumeQuoteQuantums: 80_000_000_000, + }, k.GetUserStats(ctx, "referrer_for_taker")) + + // Verify maker's referrer also received the attributed volume + assert.Equal(t, &types.UserStats{ + Affiliate_30DReferredVolumeQuoteQuantums: 80_000_000_000, + }, k.GetUserStats(ctx, "referrer_for_maker")) + + // Verify both referrers are in epoch stats + epochStats = k.GetEpochStatsOrNil(ctx, 1) + require.NotNil(t, epochStats) + + var takerReferrerFound, makerReferrerFound bool + for _, userStats := range epochStats.Stats { + if userStats.User == "referrer_for_taker" { + takerReferrerFound = true + assert.Equal(t, uint64(80_000_000_000), userStats.Stats.Affiliate_30DReferredVolumeQuoteQuantums) + } + if userStats.User == "referrer_for_maker" { + makerReferrerFound = true + assert.Equal(t, uint64(80_000_000_000), userStats.Stats.Affiliate_30DReferredVolumeQuoteQuantums) + } + } + require.True(t, takerReferrerFound, "taker's referrer should be in epoch stats") + require.True(t, makerReferrerFound, "maker's referrer should be in epoch stats") + + // Verify taker5 and maker5 stats (they're different addresses) + taker5Stats := k.GetUserStats(ctx, "taker5") + assert.Equal(t, &types.UserStats{ + TakerNotional: 80_000_000_000, + Affiliate_30DRevenueGeneratedQuantums: 40_000_000, + Affiliate_30DAttributedVolumeQuoteQuantums: 80_000_000_000, // Taker's volume attributed + }, taker5Stats) + + maker5Stats := k.GetUserStats(ctx, "maker5") + assert.Equal(t, &types.UserStats{ + MakerNotional: 80_000_000_000, + Affiliate_30DAttributedVolumeQuoteQuantums: 80_000_000_000, // Maker's volume attributed + }, maker5Stats) } func TestExpireOldStats(t *testing.T) { @@ -223,27 +506,39 @@ func TestExpireOldStats(t *testing.T) { { User: "alice", Stats: &types.UserStats{ - TakerNotional: 1, - MakerNotional: 2, + TakerNotional: 1, + MakerNotional: 2, + Affiliate_30DReferredVolumeQuoteQuantums: 10_000_000_000, + Affiliate_30DRevenueGeneratedQuantums: 100_000_000, + Affiliate_30DAttributedVolumeQuoteQuantums: 5_000_000_000, // 5k attributed per epoch }, }, { User: "bob", Stats: &types.UserStats{ - TakerNotional: 2, - MakerNotional: 1, + TakerNotional: 2, + MakerNotional: 1, + Affiliate_30DReferredVolumeQuoteQuantums: 10_000_000_000, + Affiliate_30DRevenueGeneratedQuantums: 100_000_000, + Affiliate_30DAttributedVolumeQuoteQuantums: 8_000_000_000, // 8k attributed per epoch }, }, }, }) } k.SetUserStats(ctx, "alice", &types.UserStats{ - TakerNotional: 30, - MakerNotional: 60, + TakerNotional: 30, + MakerNotional: 60, + Affiliate_30DReferredVolumeQuoteQuantums: 300_000_000_000, + Affiliate_30DRevenueGeneratedQuantums: 3_000_000_000, + Affiliate_30DAttributedVolumeQuoteQuantums: 150_000_000_000, // 30 epochs * 5k per epoch }) k.SetUserStats(ctx, "bob", &types.UserStats{ - TakerNotional: 60, - MakerNotional: 30, + TakerNotional: 60, + MakerNotional: 30, + Affiliate_30DReferredVolumeQuoteQuantums: 300_000_000_000, + Affiliate_30DRevenueGeneratedQuantums: 3_000_000_000, + Affiliate_30DAttributedVolumeQuoteQuantums: 240_000_000_000, // 30 epochs * 8k per epoch }) k.SetGlobalStats(ctx, &types.GlobalStats{ NotionalTraded: 90, @@ -260,12 +555,20 @@ func TestExpireOldStats(t *testing.T) { k.ExpireOldStats(ctx) require.Equal(t, &types.UserStats{ - TakerNotional: 30 - uint64(i+1), - MakerNotional: 60 - 2*uint64(i+1), + TakerNotional: 30 - uint64(i+1), + MakerNotional: 60 - 2*uint64(i+1), + Affiliate_30DReferredVolumeQuoteQuantums: 300_000_000_000 - (uint64(i+1) * 10_000_000_000), + Affiliate_30DRevenueGeneratedQuantums: 3_000_000_000 - (uint64(i+1) * 100_000_000), + Affiliate_30DAttributedVolumeQuoteQuantums: 150_000_000_000 - (uint64(i+1) * 5_000_000_000), + // Decreases by 5k per expired epoch }, k.GetUserStats(ctx, "alice")) require.Equal(t, &types.UserStats{ - TakerNotional: 60 - 2*uint64(i+1), - MakerNotional: 30 - uint64(i+1), + TakerNotional: 60 - 2*uint64(i+1), + MakerNotional: 30 - uint64(i+1), + Affiliate_30DReferredVolumeQuoteQuantums: 300_000_000_000 - (uint64(i+1) * 10_000_000_000), + Affiliate_30DRevenueGeneratedQuantums: 3_000_000_000 - (uint64(i+1) * 100_000_000), + Affiliate_30DAttributedVolumeQuoteQuantums: 240_000_000_000 - (uint64(i+1) * 8_000_000_000), + // Decreases by 8k per expired epoch }, k.GetUserStats(ctx, "bob")) require.Equal(t, &types.GlobalStats{ NotionalTraded: 90 - 3*uint64(i+1), @@ -278,12 +581,18 @@ func TestExpireOldStats(t *testing.T) { // Unchanged after pruning nil epoch require.Equal(t, &types.UserStats{ - TakerNotional: 30 - uint64(i+1), - MakerNotional: 60 - 2*uint64(i+1), + TakerNotional: 30 - uint64(i+1), + MakerNotional: 60 - 2*uint64(i+1), + Affiliate_30DReferredVolumeQuoteQuantums: 300_000_000_000 - (uint64(i+1) * 10_000_000_000), + Affiliate_30DRevenueGeneratedQuantums: 3_000_000_000 - (uint64(i+1) * 100_000_000), + Affiliate_30DAttributedVolumeQuoteQuantums: 150_000_000_000 - (uint64(i+1) * 5_000_000_000), }, k.GetUserStats(ctx, "alice")) require.Equal(t, &types.UserStats{ - TakerNotional: 60 - 2*uint64(i+1), - MakerNotional: 30 - uint64(i+1), + TakerNotional: 60 - 2*uint64(i+1), + MakerNotional: 30 - uint64(i+1), + Affiliate_30DReferredVolumeQuoteQuantums: 300_000_000_000 - (uint64(i+1) * 10_000_000_000), + Affiliate_30DRevenueGeneratedQuantums: 3_000_000_000 - (uint64(i+1) * 100_000_000), + Affiliate_30DAttributedVolumeQuoteQuantums: 240_000_000_000 - (uint64(i+1) * 8_000_000_000), }, k.GetUserStats(ctx, "bob")) require.Equal(t, &types.GlobalStats{ NotionalTraded: 90 - 3*uint64(i+1), @@ -297,6 +606,266 @@ func TestExpireOldStats(t *testing.T) { require.NotNil(t, k.GetEpochStatsOrNil(ctx, uint32(12))) } +// TestAffiliateAttribution_ConsistentlyHighVolumeTrader tests the scenario where a user +// is consistently trading at high volume and hitting the attribution cap. +// This test proves there is NO equilibrium trap - the user can continue getting +// attribution as old stats expire, even while trading continuously. +func TestAffiliateAttribution_ConsistentlyHighVolumeTrader(t *testing.T) { + tApp := testapp.NewTestAppBuilder(t).Build() + + // Epochs start at block height 2 + ctx := tApp.AdvanceToBlock(2, testapp.AdvanceToBlockOptions{ + BlockTime: time.Unix(int64(1), 0).UTC(), + }) + + // Advance time so first 5 epochs will be ready to expire + windowDuration := tApp.App.StatsKeeper.GetWindowDuration(ctx) + tApp.AdvanceToBlock(3, testapp.AdvanceToBlockOptions{ + BlockTime: time.Unix(0, 0). + Add(windowDuration). + Add((time.Duration(5*epochstypes.StatsEpochDuration) + 1) * time.Second). + UTC(), + }) + + // Now advance to a stable block and set up stats + ctx = tApp.AdvanceToBlock(100, testapp.AdvanceToBlockOptions{}) + k := tApp.App.StatsKeeper + + // Scenario: User is a consistent high-volume trader + // - Cap is 100k + // - User has been trading 200k per epoch for 30 epochs + // - User has attributed 100k total (at cap) + // - Old epochs will expire, allowing new attribution + + // Create 30 epochs of history where the user traded 200k each epoch + // but only 100k total was attributed (due to cap being reached early) + for i := 0; i < 30; i++ { + var attributedThisEpoch uint64 + if i < 5 { + // First 5 epochs: 20k attributed per epoch = 100k total (reaches cap) + attributedThisEpoch = 20_000_000_000 + } else { + // Epochs 6-30: 0 attributed (already at cap) + attributedThisEpoch = 0 + } + + k.SetEpochStats(ctx, uint32(i*2), &types.EpochStats{ + EpochEndTime: time.Unix(0, 0). + Add(time.Duration(i*int(epochstypes.StatsEpochDuration)) * time.Second). + UTC(), + Stats: []*types.EpochStats_UserWithStats{ + { + User: "highVolumeTrader", + Stats: &types.UserStats{ + TakerNotional: 200_000_000_000, // 200k traded + MakerNotional: 0, + Affiliate_30DReferredVolumeQuoteQuantums: 0, + Affiliate_30DRevenueGeneratedQuantums: 0, + Affiliate_30DAttributedVolumeQuoteQuantums: attributedThisEpoch, // Only first + // 5 epochs have attribution + }, + }, + { + User: "someAffiliate", + Stats: &types.UserStats{ + TakerNotional: 0, + MakerNotional: 0, + Affiliate_30DReferredVolumeQuoteQuantums: attributedThisEpoch, // Same as attributed + // (this is what the affiliate refers) + Affiliate_30DRevenueGeneratedQuantums: 0, + }, + }, + }, + }) + } + + // User's current stats: 6000k traded (30 epochs × 200k), but only 100k attributed + k.SetUserStats(ctx, "highVolumeTrader", &types.UserStats{ + TakerNotional: 6000_000_000_000, // 6M total volume + MakerNotional: 0, + Affiliate_30DReferredVolumeQuoteQuantums: 0, + Affiliate_30DRevenueGeneratedQuantums: 0, + Affiliate_30DAttributedVolumeQuoteQuantums: 100_000_000_000, // At cap (100k) + }) + + k.SetStatsMetadata(ctx, &types.StatsMetadata{ + TrailingEpoch: 0, + }) + + // Set up affiliate's initial referred volume (also at cap from same history) + k.SetUserStats(ctx, "someAffiliate", &types.UserStats{ + TakerNotional: 0, + MakerNotional: 0, + Affiliate_30DReferredVolumeQuoteQuantums: 100_000_000_000, // Also at 100k + Affiliate_30DRevenueGeneratedQuantums: 0, + }) + + // BEFORE expiration: User is at cap (100k) + userStats := k.GetUserStats(ctx, "highVolumeTrader") + require.Equal(t, uint64(100_000_000_000), userStats.Affiliate_30DAttributedVolumeQuoteQuantums, + "User should start at cap") + + affiliateStats := k.GetUserStats(ctx, "someAffiliate") + require.Equal(t, uint64(100_000_000_000), affiliateStats.Affiliate_30DReferredVolumeQuoteQuantums, + "Affiliate should start with 100k referred volume") + + // NOW INTERWEAVE: Expire old stats while processing new blocks with attribution + // This simulates the realistic scenario where a high-volume trader continues trading + // as old attributed volume expires, keeping their attributed volume at/near the cap + + // Expire first epoch (20k attributed removed) + Process new block (20k attributed added) + k.ExpireOldStats(ctx) // Removes 20k from epoch 0 + k.SetBlockStats(ctx, &types.BlockStats{ + Fills: []*types.BlockStats_Fill{ + { + Taker: "highVolumeTrader", + Maker: "someMaker", + Notional: 20_000_000_000, + AffiliateAttributions: []*types.AffiliateAttribution{ + { + Role: types.AffiliateAttribution_ROLE_TAKER, + ReferrerAddress: "someAffiliate", + ReferredVolumeQuoteQuantums: 20_000_000_000, + }, + }, + }, + }, + }) + k.ProcessBlockStats(ctx) + + userStats = k.GetUserStats(ctx, "highVolumeTrader") + require.Equal(t, uint64(100_000_000_000), userStats.Affiliate_30DAttributedVolumeQuoteQuantums, + "Attributed volume stays at cap: 100k - 20k (expired) + 20k (new) = 100k") + + affiliateStats = k.GetUserStats(ctx, "someAffiliate") + require.Equal(t, uint64(100_000_000_000), affiliateStats.Affiliate_30DReferredVolumeQuoteQuantums, + "Affiliate referred volume stays at maximum: 100k") + + // Skip nil epoch + k.ExpireOldStats(ctx) + + // Expire second epoch (20k removed) + Process new block (20k added) + k.ExpireOldStats(ctx) // Removes 20k from epoch 2 + k.SetBlockStats(ctx, &types.BlockStats{ + Fills: []*types.BlockStats_Fill{ + { + Taker: "highVolumeTrader", + Maker: "someMaker", + Notional: 20_000_000_000, + AffiliateAttributions: []*types.AffiliateAttribution{ + { + Role: types.AffiliateAttribution_ROLE_TAKER, + ReferrerAddress: "someAffiliate", + ReferredVolumeQuoteQuantums: 20_000_000_000, + }, + }, + }, + }, + }) + k.ProcessBlockStats(ctx) + + userStats = k.GetUserStats(ctx, "highVolumeTrader") + require.Equal(t, uint64(100_000_000_000), userStats.Affiliate_30DAttributedVolumeQuoteQuantums, + "Attributed volume stays at cap: still 100k after second rotation") + + affiliateStats = k.GetUserStats(ctx, "someAffiliate") + require.Equal(t, uint64(100_000_000_000), affiliateStats.Affiliate_30DReferredVolumeQuoteQuantums, + "Affiliate referred volume stays at maximum: 100k") + + // Skip nil epoch + k.ExpireOldStats(ctx) + + // Expire third epoch (20k removed) + Process new block (20k added) + k.ExpireOldStats(ctx) + k.SetBlockStats(ctx, &types.BlockStats{ + Fills: []*types.BlockStats_Fill{ + { + Taker: "highVolumeTrader", + Maker: "someMaker", + Notional: 20_000_000_000, + AffiliateAttributions: []*types.AffiliateAttribution{ + { + Role: types.AffiliateAttribution_ROLE_TAKER, + ReferrerAddress: "someAffiliate", + ReferredVolumeQuoteQuantums: 20_000_000_000, + }, + }, + }, + }, + }) + k.ProcessBlockStats(ctx) + + userStats = k.GetUserStats(ctx, "highVolumeTrader") + require.Equal(t, uint64(100_000_000_000), userStats.Affiliate_30DAttributedVolumeQuoteQuantums, + "Attributed volume stays at cap: still 100k after third rotation") + + affiliateStats = k.GetUserStats(ctx, "someAffiliate") + require.Equal(t, uint64(100_000_000_000), affiliateStats.Affiliate_30DReferredVolumeQuoteQuantums, + "Affiliate referred volume stays at maximum: 100k") + + // Skip nil epoch + k.ExpireOldStats(ctx) + + // Expire fourth epoch (20k removed) + Process new block (20k added) + k.ExpireOldStats(ctx) + k.SetBlockStats(ctx, &types.BlockStats{ + Fills: []*types.BlockStats_Fill{ + { + Taker: "highVolumeTrader", + Maker: "someMaker", + Notional: 20_000_000_000, + AffiliateAttributions: []*types.AffiliateAttribution{ + { + Role: types.AffiliateAttribution_ROLE_TAKER, + ReferrerAddress: "someAffiliate", + ReferredVolumeQuoteQuantums: 20_000_000_000, + }, + }, + }, + }, + }) + k.ProcessBlockStats(ctx) + + userStats = k.GetUserStats(ctx, "highVolumeTrader") + require.Equal(t, uint64(100_000_000_000), userStats.Affiliate_30DAttributedVolumeQuoteQuantums, + "Attributed volume stays at cap: still 100k after fourth rotation") + + affiliateStats = k.GetUserStats(ctx, "someAffiliate") + require.Equal(t, uint64(100_000_000_000), affiliateStats.Affiliate_30DReferredVolumeQuoteQuantums, + "Affiliate referred volume stays at maximum: 100k") + + // Skip nil epoch + k.ExpireOldStats(ctx) + + // Expire fifth epoch (20k removed) + Process new block (20k added) + k.ExpireOldStats(ctx) + k.SetBlockStats(ctx, &types.BlockStats{ + Fills: []*types.BlockStats_Fill{ + { + Taker: "highVolumeTrader", + Maker: "someMaker", + Notional: 20_000_000_000, + AffiliateAttributions: []*types.AffiliateAttribution{ + { + Role: types.AffiliateAttribution_ROLE_TAKER, + ReferrerAddress: "someAffiliate", + ReferredVolumeQuoteQuantums: 20_000_000_000, + }, + }, + }, + }, + }) + k.ProcessBlockStats(ctx) + + userStats = k.GetUserStats(ctx, "highVolumeTrader") + require.Equal(t, uint64(100_000_000_000), userStats.Affiliate_30DAttributedVolumeQuoteQuantums, + "Attributed volume STAYS AT MAXIMUM: 100k throughout the entire rotation!") + + affiliateStats = k.GetUserStats(ctx, "someAffiliate") + require.Equal(t, uint64(100_000_000_000), affiliateStats.Affiliate_30DReferredVolumeQuoteQuantums, + "Affiliate referred volume STAYS AT MAXIMUM: 100k throughout the entire rotation!") +} + func TestGetStakedBaseTokens(t *testing.T) { testCases := []struct { name string diff --git a/protocol/x/stats/types/stats.pb.go b/protocol/x/stats/types/stats.pb.go index f9fa866ae3..9cde22d8ca 100644 --- a/protocol/x/stats/types/stats.pb.go +++ b/protocol/x/stats/types/stats.pb.go @@ -28,6 +28,99 @@ var _ = time.Kitchen // proto package needs to be updated. const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package +// Role indicates whether this attribution is for the taker or maker +type AffiliateAttribution_Role int32 + +const ( + AffiliateAttribution_ROLE_UNSPECIFIED AffiliateAttribution_Role = 0 + AffiliateAttribution_ROLE_TAKER AffiliateAttribution_Role = 1 + AffiliateAttribution_ROLE_MAKER AffiliateAttribution_Role = 2 +) + +var AffiliateAttribution_Role_name = map[int32]string{ + 0: "ROLE_UNSPECIFIED", + 1: "ROLE_TAKER", + 2: "ROLE_MAKER", +} + +var AffiliateAttribution_Role_value = map[string]int32{ + "ROLE_UNSPECIFIED": 0, + "ROLE_TAKER": 1, + "ROLE_MAKER": 2, +} + +func (x AffiliateAttribution_Role) String() string { + return proto.EnumName(AffiliateAttribution_Role_name, int32(x)) +} + +func (AffiliateAttribution_Role) EnumDescriptor() ([]byte, []int) { + return fileDescriptor_07475747e6dcccdc, []int{0, 0} +} + +// AffiliateAttribution represents the affiliate attribution for a fill. +type AffiliateAttribution struct { + // Role of the trader (taker or maker) whose affiliate is being attributed + Role AffiliateAttribution_Role `protobuf:"varint,1,opt,name=role,proto3,enum=dydxprotocol.stats.AffiliateAttribution_Role" json:"role,omitempty"` + // Referrer address (the affiliate receiving the fee) + ReferrerAddress string `protobuf:"bytes,2,opt,name=referrer_address,json=referrerAddress,proto3" json:"referrer_address,omitempty"` + // Referred volume in quote quantums (capped based on 30-day volume limits) + ReferredVolumeQuoteQuantums uint64 `protobuf:"varint,3,opt,name=referred_volume_quote_quantums,json=referredVolumeQuoteQuantums,proto3" json:"referred_volume_quote_quantums,omitempty"` +} + +func (m *AffiliateAttribution) Reset() { *m = AffiliateAttribution{} } +func (m *AffiliateAttribution) String() string { return proto.CompactTextString(m) } +func (*AffiliateAttribution) ProtoMessage() {} +func (*AffiliateAttribution) Descriptor() ([]byte, []int) { + return fileDescriptor_07475747e6dcccdc, []int{0} +} +func (m *AffiliateAttribution) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *AffiliateAttribution) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_AffiliateAttribution.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *AffiliateAttribution) XXX_Merge(src proto.Message) { + xxx_messageInfo_AffiliateAttribution.Merge(m, src) +} +func (m *AffiliateAttribution) XXX_Size() int { + return m.Size() +} +func (m *AffiliateAttribution) XXX_DiscardUnknown() { + xxx_messageInfo_AffiliateAttribution.DiscardUnknown(m) +} + +var xxx_messageInfo_AffiliateAttribution proto.InternalMessageInfo + +func (m *AffiliateAttribution) GetRole() AffiliateAttribution_Role { + if m != nil { + return m.Role + } + return AffiliateAttribution_ROLE_UNSPECIFIED +} + +func (m *AffiliateAttribution) GetReferrerAddress() string { + if m != nil { + return m.ReferrerAddress + } + return "" +} + +func (m *AffiliateAttribution) GetReferredVolumeQuoteQuantums() uint64 { + if m != nil { + return m.ReferredVolumeQuoteQuantums + } + return 0 +} + // BlockStats is used to store stats transiently within the scope of a block. type BlockStats struct { // The fills that occured on this block. @@ -38,7 +131,7 @@ func (m *BlockStats) Reset() { *m = BlockStats{} } func (m *BlockStats) String() string { return proto.CompactTextString(m) } func (*BlockStats) ProtoMessage() {} func (*BlockStats) Descriptor() ([]byte, []int) { - return fileDescriptor_07475747e6dcccdc, []int{0} + return fileDescriptor_07475747e6dcccdc, []int{1} } func (m *BlockStats) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -87,13 +180,16 @@ type BlockStats_Fill struct { // Used to calculate affiliate revenue attributed for taker. This is dynamic // per affiliate tier AffiliateFeeGeneratedQuantums uint64 `protobuf:"varint,4,opt,name=affiliate_fee_generated_quantums,json=affiliateFeeGeneratedQuantums,proto3" json:"affiliate_fee_generated_quantums,omitempty"` + // Affiliate revenue attributions for this fill (can include both taker and + // maker) + AffiliateAttributions []*AffiliateAttribution `protobuf:"bytes,5,rep,name=affiliate_attributions,json=affiliateAttributions,proto3" json:"affiliate_attributions,omitempty"` } func (m *BlockStats_Fill) Reset() { *m = BlockStats_Fill{} } func (m *BlockStats_Fill) String() string { return proto.CompactTextString(m) } func (*BlockStats_Fill) ProtoMessage() {} func (*BlockStats_Fill) Descriptor() ([]byte, []int) { - return fileDescriptor_07475747e6dcccdc, []int{0, 0} + return fileDescriptor_07475747e6dcccdc, []int{1, 0} } func (m *BlockStats_Fill) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -150,6 +246,13 @@ func (m *BlockStats_Fill) GetAffiliateFeeGeneratedQuantums() uint64 { return 0 } +func (m *BlockStats_Fill) GetAffiliateAttributions() []*AffiliateAttribution { + if m != nil { + return m.AffiliateAttributions + } + return nil +} + // StatsMetadata stores metadata for the x/stats module type StatsMetadata struct { // The oldest epoch that is included in the stats. The next epoch to be @@ -161,7 +264,7 @@ func (m *StatsMetadata) Reset() { *m = StatsMetadata{} } func (m *StatsMetadata) String() string { return proto.CompactTextString(m) } func (*StatsMetadata) ProtoMessage() {} func (*StatsMetadata) Descriptor() ([]byte, []int) { - return fileDescriptor_07475747e6dcccdc, []int{1} + return fileDescriptor_07475747e6dcccdc, []int{2} } func (m *StatsMetadata) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -209,7 +312,7 @@ func (m *EpochStats) Reset() { *m = EpochStats{} } func (m *EpochStats) String() string { return proto.CompactTextString(m) } func (*EpochStats) ProtoMessage() {} func (*EpochStats) Descriptor() ([]byte, []int) { - return fileDescriptor_07475747e6dcccdc, []int{2} + return fileDescriptor_07475747e6dcccdc, []int{3} } func (m *EpochStats) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -262,7 +365,7 @@ func (m *EpochStats_UserWithStats) Reset() { *m = EpochStats_UserWithSta func (m *EpochStats_UserWithStats) String() string { return proto.CompactTextString(m) } func (*EpochStats_UserWithStats) ProtoMessage() {} func (*EpochStats_UserWithStats) Descriptor() ([]byte, []int) { - return fileDescriptor_07475747e6dcccdc, []int{2, 0} + return fileDescriptor_07475747e6dcccdc, []int{3, 0} } func (m *EpochStats_UserWithStats) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -315,7 +418,7 @@ func (m *GlobalStats) Reset() { *m = GlobalStats{} } func (m *GlobalStats) String() string { return proto.CompactTextString(m) } func (*GlobalStats) ProtoMessage() {} func (*GlobalStats) Descriptor() ([]byte, []int) { - return fileDescriptor_07475747e6dcccdc, []int{3} + return fileDescriptor_07475747e6dcccdc, []int{4} } func (m *GlobalStats) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -362,13 +465,16 @@ type UserStats struct { Affiliate_30DRevenueGeneratedQuantums uint64 `protobuf:"varint,3,opt,name=affiliate_30d_revenue_generated_quantums,json=affiliate30dRevenueGeneratedQuantums,proto3" json:"affiliate_30d_revenue_generated_quantums,omitempty"` // Referred volume in quote quantums with this user being an affiliate Affiliate_30DReferredVolumeQuoteQuantums uint64 `protobuf:"varint,4,opt,name=affiliate_30d_referred_volume_quote_quantums,json=affiliate30dReferredVolumeQuoteQuantums,proto3" json:"affiliate_30d_referred_volume_quote_quantums,omitempty"` + // Attributed volume in quote quantums - volume from this user (as referee) + // that has been attributed to their affiliate in the last 30 days + Affiliate_30DAttributedVolumeQuoteQuantums uint64 `protobuf:"varint,5,opt,name=affiliate_30d_attributed_volume_quote_quantums,json=affiliate30dAttributedVolumeQuoteQuantums,proto3" json:"affiliate_30d_attributed_volume_quote_quantums,omitempty"` } func (m *UserStats) Reset() { *m = UserStats{} } func (m *UserStats) String() string { return proto.CompactTextString(m) } func (*UserStats) ProtoMessage() {} func (*UserStats) Descriptor() ([]byte, []int) { - return fileDescriptor_07475747e6dcccdc, []int{4} + return fileDescriptor_07475747e6dcccdc, []int{5} } func (m *UserStats) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -425,6 +531,13 @@ func (m *UserStats) GetAffiliate_30DReferredVolumeQuoteQuantums() uint64 { return 0 } +func (m *UserStats) GetAffiliate_30DAttributedVolumeQuoteQuantums() uint64 { + if m != nil { + return m.Affiliate_30DAttributedVolumeQuoteQuantums + } + return 0 +} + // CachedStakedBaseTokens stores the last calculated total staked base tokens type CachedStakedBaseTokens struct { // Last calculated total staked base tokens by the delegator. @@ -438,7 +551,7 @@ func (m *CachedStakedBaseTokens) Reset() { *m = CachedStakedBaseTokens{} func (m *CachedStakedBaseTokens) String() string { return proto.CompactTextString(m) } func (*CachedStakedBaseTokens) ProtoMessage() {} func (*CachedStakedBaseTokens) Descriptor() ([]byte, []int) { - return fileDescriptor_07475747e6dcccdc, []int{5} + return fileDescriptor_07475747e6dcccdc, []int{6} } func (m *CachedStakedBaseTokens) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -475,6 +588,8 @@ func (m *CachedStakedBaseTokens) GetCachedAt() int64 { } func init() { + proto.RegisterEnum("dydxprotocol.stats.AffiliateAttribution_Role", AffiliateAttribution_Role_name, AffiliateAttribution_Role_value) + proto.RegisterType((*AffiliateAttribution)(nil), "dydxprotocol.stats.AffiliateAttribution") proto.RegisterType((*BlockStats)(nil), "dydxprotocol.stats.BlockStats") proto.RegisterType((*BlockStats_Fill)(nil), "dydxprotocol.stats.BlockStats.Fill") proto.RegisterType((*StatsMetadata)(nil), "dydxprotocol.stats.StatsMetadata") @@ -488,49 +603,99 @@ func init() { func init() { proto.RegisterFile("dydxprotocol/stats/stats.proto", fileDescriptor_07475747e6dcccdc) } var fileDescriptor_07475747e6dcccdc = []byte{ - // 659 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x94, 0xcf, 0x4e, 0xdb, 0x4a, - 0x14, 0xc6, 0xe3, 0x10, 0xae, 0x60, 0x20, 0xb9, 0x57, 0x23, 0x74, 0x15, 0xe5, 0x8a, 0x24, 0xca, - 0x2d, 0x22, 0x0b, 0xea, 0x20, 0x52, 0x51, 0x75, 0xd7, 0xa6, 0x02, 0xda, 0x4a, 0xad, 0x84, 0xa1, - 0xb4, 0xaa, 0x54, 0x8d, 0x26, 0x9e, 0x13, 0xc7, 0x62, 0xec, 0x09, 0xf6, 0x38, 0x82, 0x3e, 0x05, - 0xbb, 0x3e, 0x45, 0xdf, 0x83, 0x25, 0xcb, 0xaa, 0x0b, 0xda, 0xc2, 0x3b, 0x74, 0x5d, 0xf9, 0x0c, - 0x76, 0x08, 0xb0, 0xe8, 0x26, 0x9a, 0xf3, 0x9b, 0xef, 0xfc, 0xc9, 0x37, 0x47, 0x26, 0x75, 0x71, - 0x22, 0x8e, 0x47, 0x91, 0xd2, 0xca, 0x55, 0xb2, 0x13, 0x6b, 0xae, 0x63, 0xf3, 0x6b, 0x23, 0xa4, - 0xf4, 0xe6, 0xbd, 0x8d, 0x37, 0xb5, 0x25, 0x4f, 0x79, 0x0a, 0x59, 0x27, 0x3d, 0x19, 0x65, 0xad, - 0xe1, 0x29, 0xe5, 0x49, 0xe8, 0x60, 0xd4, 0x4f, 0x06, 0x1d, 0xed, 0x07, 0x10, 0x6b, 0x1e, 0x8c, - 0x8c, 0xa0, 0xf5, 0xd3, 0x22, 0xa4, 0x27, 0x95, 0x7b, 0xb8, 0x97, 0x56, 0xa1, 0x4f, 0xc8, 0xec, - 0xc0, 0x97, 0x32, 0xae, 0x5a, 0xcd, 0x99, 0xf6, 0xc2, 0xc6, 0xff, 0xf6, 0xdd, 0x4e, 0xf6, 0x44, - 0x6e, 0x6f, 0xfb, 0x52, 0x3a, 0x26, 0xa3, 0xf6, 0xd9, 0x22, 0xa5, 0x34, 0xa6, 0x4b, 0x64, 0x56, - 0xf3, 0x43, 0x88, 0xaa, 0x56, 0xd3, 0x6a, 0xcf, 0x3b, 0x26, 0x48, 0x69, 0x80, 0xb4, 0x68, 0x28, - 0x06, 0xb4, 0x46, 0xe6, 0x42, 0xa5, 0x7d, 0x15, 0x72, 0x59, 0x9d, 0x69, 0x5a, 0xed, 0x92, 0x93, - 0xc7, 0x74, 0x87, 0x34, 0xf9, 0x60, 0xe0, 0x4b, 0x9f, 0x6b, 0x60, 0x03, 0x00, 0xe6, 0x41, 0x08, - 0x11, 0xd7, 0x20, 0xd8, 0x51, 0xc2, 0x43, 0x9d, 0x04, 0x71, 0xb5, 0x84, 0x39, 0xcb, 0xb9, 0x6e, - 0x1b, 0x60, 0x27, 0x53, 0xed, 0x5e, 0x8b, 0x5a, 0x9b, 0xa4, 0x8c, 0xe3, 0xbe, 0x06, 0xcd, 0x05, - 0xd7, 0x9c, 0xae, 0x90, 0x8a, 0x8e, 0xb8, 0x2f, 0xfd, 0xd0, 0x63, 0x30, 0x52, 0xee, 0x10, 0x47, - 0x2d, 0x3b, 0xe5, 0x8c, 0x6e, 0xa5, 0xb0, 0xf5, 0xcb, 0x22, 0x04, 0x4f, 0xc6, 0x9b, 0x57, 0xa4, - 0x82, 0x62, 0x06, 0xa1, 0x60, 0xa9, 0x8f, 0x98, 0xb5, 0xb0, 0x51, 0xb3, 0x8d, 0xc9, 0x76, 0x66, - 0xb2, 0xbd, 0x9f, 0x99, 0xdc, 0x9b, 0x3b, 0xbb, 0x68, 0x14, 0x4e, 0xbf, 0x37, 0x2c, 0x67, 0x11, - 0x73, 0xb7, 0x42, 0x91, 0x5e, 0xd2, 0x1e, 0x99, 0x45, 0x33, 0xab, 0x45, 0xf4, 0x79, 0xed, 0x3e, - 0x9f, 0x27, 0xad, 0xed, 0xb7, 0x31, 0x44, 0xef, 0x7c, 0x6d, 0x22, 0xc7, 0xa4, 0xd6, 0xde, 0x93, - 0xf2, 0x14, 0xa7, 0x94, 0x94, 0x92, 0x38, 0xf7, 0x1d, 0xcf, 0xb4, 0x3b, 0x69, 0x94, 0xce, 0xba, - 0x7c, 0x5f, 0xa3, 0xb4, 0xca, 0xcd, 0xca, 0xad, 0x4d, 0xb2, 0xb0, 0x23, 0x55, 0x9f, 0x4b, 0x53, - 0x77, 0x95, 0xfc, 0x9d, 0x3d, 0x0a, 0xd3, 0x11, 0x17, 0x20, 0xb0, 0x45, 0xc9, 0xa9, 0x64, 0x78, - 0x1f, 0x69, 0xeb, 0xb4, 0x48, 0xe6, 0xf3, 0x62, 0xe8, 0x72, 0xfa, 0xc8, 0x2c, 0x7f, 0x61, 0x93, - 0x55, 0x46, 0xfa, 0x26, 0x7b, 0xe6, 0x15, 0x52, 0x09, 0xa6, 0x65, 0x45, 0x23, 0x0b, 0xa6, 0x64, - 0x07, 0xa4, 0x3d, 0xd9, 0x86, 0xee, 0xba, 0x60, 0x11, 0x8c, 0x21, 0x4c, 0xee, 0xdd, 0x0a, 0xb3, - 0x49, 0x0f, 0x72, 0x7d, 0x77, 0x5d, 0x38, 0x46, 0x7d, 0x67, 0x39, 0xe8, 0x47, 0xb2, 0x76, 0xbb, - 0xee, 0x00, 0xa2, 0x08, 0x04, 0x1b, 0x2b, 0x99, 0x04, 0xc0, 0x8e, 0x12, 0xa5, 0xe1, 0xf6, 0xc6, - 0xad, 0x4e, 0xd7, 0x36, 0x19, 0x07, 0x98, 0xb0, 0x9b, 0xea, 0xf3, 0xdd, 0xfb, 0x62, 0x91, 0x7f, - 0x9f, 0x73, 0x77, 0x08, 0x62, 0x2f, 0xfd, 0xdb, 0xa2, 0xc7, 0x63, 0xd8, 0x57, 0x87, 0x10, 0xc6, - 0x74, 0x4c, 0x68, 0x8c, 0x8c, 0xf5, 0x79, 0x0c, 0x4c, 0x23, 0x45, 0x8f, 0x16, 0x7b, 0x2f, 0xd2, - 0xbd, 0xf9, 0x76, 0xd1, 0x78, 0xea, 0xf9, 0x7a, 0x98, 0xf4, 0x6d, 0x57, 0x05, 0x9d, 0xa9, 0x8f, - 0xc2, 0xf8, 0xd1, 0x43, 0x77, 0xc8, 0xfd, 0xb0, 0x93, 0x13, 0xa1, 0x4f, 0x46, 0x10, 0xdb, 0x7b, - 0x10, 0xf9, 0x5c, 0xfa, 0x9f, 0x78, 0x5f, 0xc2, 0xcb, 0x50, 0x3b, 0xff, 0xc4, 0xb7, 0xfb, 0xfe, - 0x47, 0xe6, 0x5d, 0x9c, 0x88, 0x71, 0x8d, 0x5e, 0xcf, 0x38, 0x73, 0x06, 0x3c, 0xd3, 0xbd, 0xdd, - 0xb3, 0xcb, 0xba, 0x75, 0x7e, 0x59, 0xb7, 0x7e, 0x5c, 0xd6, 0xad, 0xd3, 0xab, 0x7a, 0xe1, 0xfc, - 0xaa, 0x5e, 0xf8, 0x7a, 0x55, 0x2f, 0x7c, 0x78, 0xfc, 0xe7, 0xa3, 0x1c, 0x5f, 0x7f, 0xb3, 0x70, - 0xa2, 0xfe, 0x5f, 0xc8, 0xbb, 0xbf, 0x03, 0x00, 0x00, 0xff, 0xff, 0x78, 0xda, 0x4e, 0x4b, 0xd6, - 0x04, 0x00, 0x00, + // 824 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x55, 0x51, 0x6f, 0x1b, 0x45, + 0x10, 0xf6, 0x39, 0x36, 0x4a, 0x26, 0xb5, 0x6b, 0xad, 0x42, 0x65, 0xb9, 0xaa, 0x63, 0x19, 0xaa, + 0xba, 0x52, 0x7b, 0xae, 0x12, 0x54, 0x84, 0xc4, 0x03, 0x76, 0x70, 0x42, 0x80, 0x16, 0x72, 0x49, + 0x0b, 0x42, 0x42, 0xa7, 0xb5, 0x77, 0xec, 0x9c, 0xb2, 0xbe, 0x0d, 0xb7, 0x7b, 0x51, 0xcb, 0x1f, + 0xe0, 0xb5, 0x7f, 0x84, 0x3f, 0xc1, 0x53, 0x1f, 0xfb, 0x88, 0x78, 0x28, 0x28, 0xf9, 0x09, 0x48, + 0x88, 0x47, 0xb4, 0xb3, 0xbe, 0x73, 0xdc, 0xba, 0x28, 0x2f, 0xd6, 0xcd, 0xb7, 0xdf, 0xcc, 0xec, + 0x7c, 0xf3, 0xdd, 0x19, 0x9a, 0xe2, 0xb9, 0x78, 0x76, 0x9a, 0x28, 0xa3, 0x46, 0x4a, 0x76, 0xb5, + 0xe1, 0x46, 0xbb, 0x5f, 0x9f, 0x40, 0xc6, 0x2e, 0x9f, 0xfb, 0x74, 0xd2, 0xd8, 0x98, 0xa8, 0x89, + 0x22, 0xac, 0x6b, 0x9f, 0x1c, 0xb3, 0xb1, 0x39, 0x51, 0x6a, 0x22, 0xb1, 0x4b, 0xd1, 0x30, 0x1d, + 0x77, 0x4d, 0x34, 0x45, 0x6d, 0xf8, 0xf4, 0xd4, 0x11, 0xda, 0xbf, 0x14, 0x61, 0xa3, 0x37, 0x1e, + 0x47, 0x32, 0xe2, 0x06, 0x7b, 0xc6, 0x24, 0xd1, 0x30, 0x35, 0x91, 0x8a, 0x59, 0x0f, 0x4a, 0x89, + 0x92, 0x58, 0xf7, 0x5a, 0x5e, 0xa7, 0xba, 0x75, 0xdf, 0x7f, 0xbb, 0xa5, 0xbf, 0x2c, 0xcf, 0x0f, + 0x94, 0xc4, 0x80, 0x52, 0xd9, 0x5d, 0xa8, 0x25, 0x38, 0xc6, 0x24, 0xc1, 0x24, 0xe4, 0x42, 0x24, + 0xa8, 0x75, 0xbd, 0xd8, 0xf2, 0x3a, 0x6b, 0xc1, 0xf5, 0x0c, 0xef, 0x39, 0x98, 0xed, 0x40, 0x73, + 0x06, 0x89, 0xf0, 0x4c, 0xc9, 0x74, 0x8a, 0xe1, 0x4f, 0xa9, 0x32, 0xf6, 0x97, 0xc7, 0x26, 0x9d, + 0xea, 0xfa, 0x4a, 0xcb, 0xeb, 0x94, 0x82, 0x9b, 0x19, 0xeb, 0x29, 0x91, 0x0e, 0x2c, 0xe7, 0x60, + 0x46, 0x69, 0x7f, 0x0a, 0x25, 0xdb, 0x9d, 0x6d, 0x40, 0x2d, 0xf8, 0xe6, 0xeb, 0x41, 0xf8, 0xe4, + 0xf1, 0xe1, 0xb7, 0x83, 0x9d, 0xfd, 0xdd, 0xfd, 0xc1, 0xe7, 0xb5, 0x02, 0xab, 0x02, 0x10, 0x7a, + 0xd4, 0xfb, 0x6a, 0x10, 0xd4, 0xbc, 0x3c, 0x7e, 0x44, 0x71, 0xb1, 0xfd, 0x5b, 0x11, 0xa0, 0x2f, + 0xd5, 0xe8, 0xe4, 0xd0, 0x0e, 0xc7, 0x3e, 0x81, 0xf2, 0x38, 0x92, 0x52, 0xd7, 0xbd, 0xd6, 0x4a, + 0x67, 0x7d, 0xeb, 0x83, 0x65, 0x02, 0xcc, 0xe9, 0xfe, 0x6e, 0x24, 0x65, 0xe0, 0x32, 0x1a, 0xff, + 0x7a, 0x50, 0xb2, 0x31, 0xdb, 0x80, 0xb2, 0xe1, 0x27, 0x98, 0x90, 0x88, 0x6b, 0x81, 0x0b, 0x2c, + 0x3a, 0x25, 0xd4, 0x69, 0xe1, 0x02, 0xd6, 0x80, 0xd5, 0x58, 0x59, 0x05, 0xb9, 0x9c, 0xcd, 0x9a, + 0xc7, 0x6c, 0x0f, 0x5a, 0x3c, 0xd3, 0x3a, 0x1c, 0x23, 0x86, 0x13, 0x8c, 0x31, 0xe1, 0x06, 0xc5, + 0x5c, 0x9f, 0x12, 0xe5, 0xdc, 0xca, 0x79, 0xbb, 0x88, 0x7b, 0x19, 0x2b, 0x53, 0x88, 0x85, 0x70, + 0x63, 0x5e, 0x88, 0xcf, 0xb7, 0xa6, 0xeb, 0x65, 0x9a, 0xb2, 0x73, 0xd5, 0x35, 0x07, 0xef, 0xf3, + 0x25, 0xa8, 0x6e, 0x3f, 0x84, 0x0a, 0xe9, 0xf1, 0x08, 0x0d, 0x17, 0xdc, 0x70, 0x76, 0x1b, 0xaa, + 0x26, 0xe1, 0x91, 0x8c, 0xe2, 0x49, 0x88, 0xa7, 0x6a, 0x74, 0x4c, 0x5a, 0x54, 0x82, 0x4a, 0x86, + 0x0e, 0x2c, 0xd8, 0xfe, 0xc7, 0x03, 0xa0, 0x27, 0x27, 0xfe, 0x97, 0x50, 0x25, 0x72, 0x88, 0xb1, + 0x08, 0xad, 0x65, 0x29, 0x6b, 0x7d, 0xab, 0xe1, 0x3b, 0x3f, 0xfb, 0x99, 0x9f, 0xfd, 0xa3, 0xcc, + 0xcf, 0xfd, 0xd5, 0x97, 0xaf, 0x37, 0x0b, 0x2f, 0xfe, 0xdc, 0xf4, 0x82, 0x6b, 0x94, 0x3b, 0x88, + 0x85, 0x3d, 0x64, 0x7d, 0x28, 0xd3, 0x1c, 0xf5, 0x22, 0x8d, 0x78, 0x6f, 0xd9, 0x88, 0xf3, 0xd6, + 0xfe, 0x13, 0x8d, 0xc9, 0x77, 0x91, 0x71, 0x51, 0xe0, 0x52, 0x1b, 0xdf, 0x43, 0x65, 0x01, 0x67, + 0x0c, 0x4a, 0xa9, 0xce, 0x17, 0x4b, 0xcf, 0x6c, 0x7b, 0xde, 0xc8, 0xde, 0xf5, 0xd6, 0xb2, 0x46, + 0xb6, 0xca, 0xe5, 0xca, 0xed, 0x87, 0xb0, 0xbe, 0x27, 0xd5, 0x90, 0x4b, 0x57, 0xf7, 0x0e, 0x5c, + 0xcf, 0xb6, 0x1e, 0x9a, 0x84, 0x0b, 0x14, 0xd4, 0xa2, 0x14, 0x54, 0x33, 0xf8, 0x88, 0xd0, 0xf6, + 0xdf, 0x45, 0x58, 0xcb, 0x8b, 0x91, 0xca, 0xd6, 0x45, 0x61, 0x6e, 0x21, 0x97, 0x55, 0x21, 0xf4, + 0x71, 0xe6, 0xa3, 0xdb, 0x50, 0x9d, 0x2e, 0xd2, 0x8a, 0x8e, 0x36, 0x5d, 0xa0, 0x3d, 0x85, 0xce, + 0xdc, 0x25, 0xdb, 0x0f, 0x44, 0x98, 0xe0, 0x19, 0xc6, 0xe9, 0x52, 0xdb, 0x39, 0xab, 0x7e, 0x98, + 0xf3, 0xb7, 0x1f, 0x88, 0xc0, 0xb1, 0xdf, 0x76, 0xdf, 0x8f, 0x70, 0xef, 0xcd, 0xba, 0xff, 0xfb, + 0xca, 0x3b, 0x4b, 0xdf, 0x59, 0xac, 0xfd, 0xce, 0xd7, 0x9f, 0x71, 0xf0, 0x17, 0xcb, 0x67, 0x06, + 0x7f, 0x67, 0x83, 0x32, 0x35, 0xb8, 0x7b, 0xb9, 0x41, 0x2f, 0xcf, 0x59, 0xf6, 0x85, 0xf9, 0xd5, + 0x83, 0x1b, 0x3b, 0x7c, 0x74, 0x8c, 0xe2, 0xd0, 0x2a, 0x2b, 0xfa, 0x5c, 0xe3, 0x91, 0x3a, 0xc1, + 0x58, 0xb3, 0x33, 0x60, 0x9a, 0xb0, 0x70, 0xc8, 0x35, 0x86, 0x86, 0x50, 0x5a, 0xc3, 0xb5, 0xfe, + 0x17, 0xd6, 0x9a, 0x7f, 0xbc, 0xde, 0xfc, 0x6c, 0x12, 0x99, 0xe3, 0x74, 0xe8, 0x8f, 0xd4, 0xb4, + 0xbb, 0xf0, 0x89, 0x3f, 0xfb, 0xe8, 0xfe, 0xe8, 0x98, 0x47, 0x71, 0x37, 0x47, 0x84, 0x79, 0x7e, + 0x8a, 0xda, 0x3f, 0xc4, 0x24, 0xe2, 0x32, 0xfa, 0x99, 0x0f, 0x25, 0xee, 0xc7, 0x26, 0xa8, 0xe9, + 0x37, 0xfb, 0xde, 0x84, 0xb5, 0x11, 0xdd, 0x28, 0xe4, 0x86, 0xd6, 0xb9, 0x12, 0xac, 0x3a, 0xa0, + 0x67, 0xfa, 0x07, 0x2f, 0xcf, 0x9b, 0xde, 0xab, 0xf3, 0xa6, 0xf7, 0xd7, 0x79, 0xd3, 0x7b, 0x71, + 0xd1, 0x2c, 0xbc, 0xba, 0x68, 0x16, 0x7e, 0xbf, 0x68, 0x16, 0x7e, 0xf8, 0xf8, 0xea, 0x57, 0x79, + 0x36, 0xfb, 0x07, 0xa2, 0x1b, 0x0d, 0xdf, 0x23, 0x7c, 0xfb, 0xbf, 0x00, 0x00, 0x00, 0xff, 0xff, + 0x4b, 0xc8, 0x22, 0xdf, 0xa4, 0x06, 0x00, 0x00, +} + +func (m *AffiliateAttribution) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *AffiliateAttribution) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *AffiliateAttribution) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.ReferredVolumeQuoteQuantums != 0 { + i = encodeVarintStats(dAtA, i, uint64(m.ReferredVolumeQuoteQuantums)) + i-- + dAtA[i] = 0x18 + } + if len(m.ReferrerAddress) > 0 { + i -= len(m.ReferrerAddress) + copy(dAtA[i:], m.ReferrerAddress) + i = encodeVarintStats(dAtA, i, uint64(len(m.ReferrerAddress))) + i-- + dAtA[i] = 0x12 + } + if m.Role != 0 { + i = encodeVarintStats(dAtA, i, uint64(m.Role)) + i-- + dAtA[i] = 0x8 + } + return len(dAtA) - i, nil } func (m *BlockStats) Marshal() (dAtA []byte, err error) { @@ -590,6 +755,20 @@ func (m *BlockStats_Fill) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + if len(m.AffiliateAttributions) > 0 { + for iNdEx := len(m.AffiliateAttributions) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.AffiliateAttributions[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintStats(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x2a + } + } if m.AffiliateFeeGeneratedQuantums != 0 { i = encodeVarintStats(dAtA, i, uint64(m.AffiliateFeeGeneratedQuantums)) i-- @@ -780,6 +959,11 @@ func (m *UserStats) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + if m.Affiliate_30DAttributedVolumeQuoteQuantums != 0 { + i = encodeVarintStats(dAtA, i, uint64(m.Affiliate_30DAttributedVolumeQuoteQuantums)) + i-- + dAtA[i] = 0x28 + } if m.Affiliate_30DReferredVolumeQuoteQuantums != 0 { i = encodeVarintStats(dAtA, i, uint64(m.Affiliate_30DReferredVolumeQuoteQuantums)) i-- @@ -852,6 +1036,25 @@ func encodeVarintStats(dAtA []byte, offset int, v uint64) int { dAtA[offset] = uint8(v) return base } +func (m *AffiliateAttribution) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.Role != 0 { + n += 1 + sovStats(uint64(m.Role)) + } + l = len(m.ReferrerAddress) + if l > 0 { + n += 1 + l + sovStats(uint64(l)) + } + if m.ReferredVolumeQuoteQuantums != 0 { + n += 1 + sovStats(uint64(m.ReferredVolumeQuoteQuantums)) + } + return n +} + func (m *BlockStats) Size() (n int) { if m == nil { return 0 @@ -887,6 +1090,12 @@ func (m *BlockStats_Fill) Size() (n int) { if m.AffiliateFeeGeneratedQuantums != 0 { n += 1 + sovStats(uint64(m.AffiliateFeeGeneratedQuantums)) } + if len(m.AffiliateAttributions) > 0 { + for _, e := range m.AffiliateAttributions { + l = e.Size() + n += 1 + l + sovStats(uint64(l)) + } + } return n } @@ -966,6 +1175,9 @@ func (m *UserStats) Size() (n int) { if m.Affiliate_30DReferredVolumeQuoteQuantums != 0 { n += 1 + sovStats(uint64(m.Affiliate_30DReferredVolumeQuoteQuantums)) } + if m.Affiliate_30DAttributedVolumeQuoteQuantums != 0 { + n += 1 + sovStats(uint64(m.Affiliate_30DAttributedVolumeQuoteQuantums)) + } return n } @@ -989,6 +1201,126 @@ func sovStats(x uint64) (n int) { func sozStats(x uint64) (n int) { return sovStats(uint64((x << 1) ^ uint64((int64(x) >> 63)))) } +func (m *AffiliateAttribution) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowStats + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: AffiliateAttribution: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: AffiliateAttribution: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Role", wireType) + } + m.Role = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowStats + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Role |= AffiliateAttribution_Role(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field ReferrerAddress", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowStats + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthStats + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthStats + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.ReferrerAddress = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 3: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field ReferredVolumeQuoteQuantums", wireType) + } + m.ReferredVolumeQuoteQuantums = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowStats + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.ReferredVolumeQuoteQuantums |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + default: + iNdEx = preIndex + skippy, err := skipStats(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthStats + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} func (m *BlockStats) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 @@ -1204,6 +1536,40 @@ func (m *BlockStats_Fill) Unmarshal(dAtA []byte) error { break } } + case 5: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field AffiliateAttributions", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowStats + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthStats + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthStats + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.AffiliateAttributions = append(m.AffiliateAttributions, &AffiliateAttribution{}) + if err := m.AffiliateAttributions[len(m.AffiliateAttributions)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipStats(dAtA[iNdEx:]) @@ -1703,6 +2069,25 @@ func (m *UserStats) Unmarshal(dAtA []byte) error { break } } + case 5: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Affiliate_30DAttributedVolumeQuoteQuantums", wireType) + } + m.Affiliate_30DAttributedVolumeQuoteQuantums = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowStats + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Affiliate_30DAttributedVolumeQuoteQuantums |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } default: iNdEx = preIndex skippy, err := skipStats(dAtA[iNdEx:])