From 077fe5516803940ac174daed92fad1160879ca79 Mon Sep 17 00:00:00 2001 From: Justin Barnett <61020572+jusbar23@users.noreply.github.com> Date: Mon, 6 Oct 2025 15:22:25 -0400 Subject: [PATCH 1/3] Dont attribute new revenue if user exceeds 30d max volume and deprecate AffiliateWhitelist (#3109) (cherry picked from commit c29eea29968a5a7bdc3bedf87cc5b24c5ab2098d) # Conflicts: # protocol/app/testdata/default_genesis_state.json # protocol/scripts/affiliates/update_affiliate_parameters.py # protocol/scripts/genesis/sample_pregenesis.json # protocol/x/affiliates/keeper/keeper.go # protocol/x/affiliates/keeper/keeper_test.go # protocol/x/affiliates/types/constants.go # protocol/x/revshare/keeper/revshare_test.go --- .../src/codegen/dydxprotocol/stats/stats.ts | 68 ++- proto/dydxprotocol/stats/stats.proto | 14 +- protocol/app/app.go | 3 + .../app/testdata/default_genesis_state.json | 8 + protocol/mocks/ClobKeeper.go | 29 +- protocol/mocks/MemClobKeeper.go | 29 +- .../affiliates/update_affiliate_parameters.py | 209 +++++++++ .../scripts/genesis/sample_pregenesis.json | 8 + protocol/testing/testnet/genesis.json | 7 + protocol/testutil/memclob/keeper.go | 4 +- protocol/x/affiliates/keeper/grpc_query.go | 14 +- .../x/affiliates/keeper/grpc_query_test.go | 15 +- protocol/x/affiliates/keeper/keeper.go | 232 +++++++++- protocol/x/affiliates/keeper/keeper_test.go | 414 +++++++++++++++--- protocol/x/affiliates/types/constants.go | 8 + protocol/x/affiliates/types/errors.go | 6 +- .../x/affiliates/types/expected_keepers.go | 1 + protocol/x/affiliates/types/keys.go | 4 +- protocol/x/clob/keeper/process_operations.go | 44 +- .../x/clob/keeper/process_single_match.go | 18 +- protocol/x/clob/memclob/memclob.go | 3 +- protocol/x/clob/types/clob_keeper.go | 4 +- protocol/x/clob/types/expected_keepers.go | 9 +- protocol/x/clob/types/mem_clob_keeper.go | 4 +- protocol/x/revshare/keeper/revshare.go | 11 +- protocol/x/revshare/keeper/revshare_test.go | 99 ++++- protocol/x/stats/keeper/keeper.go | 34 +- protocol/x/stats/keeper/keeper_test.go | 15 +- protocol/x/stats/types/expected_keepers.go | 5 + protocol/x/stats/types/stats.pb.go | 155 +++++-- 30 files changed, 1238 insertions(+), 236 deletions(-) create mode 100644 protocol/scripts/affiliates/update_affiliate_parameters.py 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 1f41aed7f6b..1edf577cd54 100644 --- a/indexer/packages/v4-protos/src/codegen/dydxprotocol/stats/stats.ts +++ b/indexer/packages/v4-protos/src/codegen/dydxprotocol/stats/stats.ts @@ -21,9 +21,19 @@ export interface BlockStats_Fill { /** Maker wallet address */ maker: string; - /** Notional USDC filled in quantums */ + /** + * Notional USDC filled in quantums + * Used to calculate fee tier, and affiliate revenue attributed for taker + */ notional: Long; + /** + * Affiliate fee generated in quantums of the taker fee for the affiliate + * Used to calculate affiliate revenue attributed for taker. This is dynamic + * per affiliate tier + */ + + affiliateFeeGeneratedQuantums: Long; } /** Fill records data about a fill on this block. */ @@ -33,9 +43,19 @@ export interface BlockStats_FillSDKType { /** Maker wallet address */ maker: string; - /** Notional USDC filled in quantums */ + /** + * Notional USDC filled in quantums + * Used to calculate fee tier, and affiliate revenue attributed for taker + */ notional: Long; + /** + * Affiliate fee generated in quantums of the taker fee for the affiliate + * Used to calculate affiliate revenue attributed for taker. This is dynamic + * per affiliate tier + */ + + affiliate_fee_generated_quantums: Long; } /** StatsMetadata stores metadata for the x/stats module */ @@ -85,19 +105,22 @@ export interface EpochStats_UserWithStatsSDKType { user: string; stats?: UserStatsSDKType; } -/** GlobalStats stores global stats */ +/** GlobalStats stores global stats for the rolling window (default 30d). */ export interface GlobalStats { /** Notional USDC traded in quantums */ notionalTraded: Long; } -/** GlobalStats stores global stats */ +/** GlobalStats stores global stats for the rolling window (default 30d). */ export interface GlobalStatsSDKType { /** Notional USDC traded in quantums */ notional_traded: Long; } -/** UserStats stores stats for a User */ +/** + * UserStats stores stats for a User. This is the sum of all stats for a user in + * the rolling window (default 30d). + */ export interface UserStats { /** Taker USDC in quantums */ @@ -105,8 +128,14 @@ export interface UserStats { /** Maker USDC in quantums */ makerNotional: Long; + /** Affiliate revenue generated in quantums */ + + affiliateRevenueGeneratedQuantums: Long; } -/** UserStats stores stats for a User */ +/** + * UserStats stores stats for a User. This is the sum of all stats for a user in + * the rolling window (default 30d). + */ export interface UserStatsSDKType { /** Taker USDC in quantums */ @@ -114,6 +143,9 @@ export interface UserStatsSDKType { /** Maker USDC in quantums */ maker_notional: Long; + /** Affiliate revenue generated in quantums */ + + affiliate_revenue_generated_quantums: Long; } /** CachedStakeAmount stores the last calculated total staked amount for address */ @@ -189,7 +221,8 @@ function createBaseBlockStats_Fill(): BlockStats_Fill { return { taker: "", maker: "", - notional: Long.UZERO + notional: Long.UZERO, + affiliateFeeGeneratedQuantums: Long.UZERO }; } @@ -207,6 +240,10 @@ export const BlockStats_Fill = { writer.uint32(24).uint64(message.notional); } + if (!message.affiliateFeeGeneratedQuantums.isZero()) { + writer.uint32(32).uint64(message.affiliateFeeGeneratedQuantums); + } + return writer; }, @@ -231,6 +268,10 @@ export const BlockStats_Fill = { message.notional = (reader.uint64() as Long); break; + case 4: + message.affiliateFeeGeneratedQuantums = (reader.uint64() as Long); + break; + default: reader.skipType(tag & 7); break; @@ -245,6 +286,7 @@ export const BlockStats_Fill = { message.taker = object.taker ?? ""; 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; return message; } @@ -453,7 +495,8 @@ export const GlobalStats = { function createBaseUserStats(): UserStats { return { takerNotional: Long.UZERO, - makerNotional: Long.UZERO + makerNotional: Long.UZERO, + affiliateRevenueGeneratedQuantums: Long.UZERO }; } @@ -467,6 +510,10 @@ export const UserStats = { writer.uint32(16).uint64(message.makerNotional); } + if (!message.affiliateRevenueGeneratedQuantums.isZero()) { + writer.uint32(24).uint64(message.affiliateRevenueGeneratedQuantums); + } + return writer; }, @@ -487,6 +534,10 @@ export const UserStats = { message.makerNotional = (reader.uint64() as Long); break; + case 3: + message.affiliateRevenueGeneratedQuantums = (reader.uint64() as Long); + break; + default: reader.skipType(tag & 7); break; @@ -500,6 +551,7 @@ export const UserStats = { const message = createBaseUserStats(); message.takerNotional = object.takerNotional !== undefined && object.takerNotional !== null ? Long.fromValue(object.takerNotional) : Long.UZERO; message.makerNotional = object.makerNotional !== undefined && object.makerNotional !== null ? Long.fromValue(object.makerNotional) : Long.UZERO; + message.affiliateRevenueGeneratedQuantums = object.affiliateRevenueGeneratedQuantums !== undefined && object.affiliateRevenueGeneratedQuantums !== null ? Long.fromValue(object.affiliateRevenueGeneratedQuantums) : Long.UZERO; return message; } diff --git a/proto/dydxprotocol/stats/stats.proto b/proto/dydxprotocol/stats/stats.proto index 4d1558fa1b7..9db08c8ccdf 100644 --- a/proto/dydxprotocol/stats/stats.proto +++ b/proto/dydxprotocol/stats/stats.proto @@ -17,7 +17,13 @@ message BlockStats { string maker = 2; // Notional USDC filled in quantums + // Used to calculate fee tier, and affiliate revenue attributed for taker uint64 notional = 3; + + // Affiliate fee generated in quantums of the taker fee for the affiliate + // Used to calculate affiliate revenue attributed for taker. This is dynamic + // per affiliate tier + uint64 affiliate_fee_generated_quantums = 4; } // The fills that occured on this block. @@ -47,19 +53,23 @@ message EpochStats { repeated UserWithStats stats = 2; } -// GlobalStats stores global stats +// GlobalStats stores global stats for the rolling window (default 30d). message GlobalStats { // Notional USDC traded in quantums uint64 notional_traded = 1; } -// UserStats stores stats for a User +// UserStats stores stats for a User. This is the sum of all stats for a user in +// the rolling window (default 30d). message UserStats { // Taker USDC in quantums uint64 taker_notional = 1; // Maker USDC in quantums uint64 maker_notional = 2; + + // Affiliate revenue generated in quantums + uint64 affiliate_revenue_generated_quantums = 3; } // CachedStakeAmount stores the last calculated total staked amount for address diff --git a/protocol/app/app.go b/protocol/app/app.go index a249eda009a..9057b47ea59 100644 --- a/protocol/app/app.go +++ b/protocol/app/app.go @@ -962,6 +962,9 @@ func New( ) affiliatesModule := affiliatesmodule.NewAppModule(appCodec, app.AffiliatesKeeper) + // Register the affiliates keeper to be notified when stats expire + app.StatsKeeper.AddStatsExpirationHook(&app.AffiliatesKeeper) + app.MarketMapKeeper = *marketmapmodulekeeper.NewKeeper( runtime.NewKVStoreService(keys[marketmapmoduletypes.StoreKey]), appCodec, diff --git a/protocol/app/testdata/default_genesis_state.json b/protocol/app/testdata/default_genesis_state.json index 0ffa88aba81..922b900caf6 100644 --- a/protocol/app/testdata/default_genesis_state.json +++ b/protocol/app/testdata/default_genesis_state.json @@ -35,6 +35,14 @@ "req_referred_volume_quote_quantums": "25000000000000", "req_staked_whole_coins": 5000, "taker_fee_share_ppm": 150000 +<<<<<<< HEAD +======= + }, + { + "req_referred_volume_quote_quantums": "50000000000000", + "req_staked_whole_coins": 1e+08, + "taker_fee_share_ppm": 250000 +>>>>>>> c29eea29 (Dont attribute new revenue if user exceeds 30d max volume and deprecate AffiliateWhitelist (#3109)) } ] } diff --git a/protocol/mocks/ClobKeeper.go b/protocol/mocks/ClobKeeper.go index efb28a25298..d1b6859d763 100644 --- a/protocol/mocks/ClobKeeper.go +++ b/protocol/mocks/ClobKeeper.go @@ -6,6 +6,7 @@ import ( big "math/big" indexer_manager "github.com/dydxprotocol/v4-chain/protocol/indexer/indexer_manager" + affiliatetypes "github.com/dydxprotocol/v4-chain/protocol/x/affiliates/types" clobtypes "github.com/dydxprotocol/v4-chain/protocol/x/clob/types" log "cosmossdk.io/log" @@ -980,8 +981,8 @@ func (_m *ClobKeeper) ProcessProposerOperations(ctx types.Context, operations [] } // ProcessSingleMatch provides a mock function with given fields: ctx, matchWithOrders, affiliatesWhitelistMap -func (_m *ClobKeeper) ProcessSingleMatch(ctx types.Context, matchWithOrders *clobtypes.MatchWithOrders, affiliatesWhitelistMap map[string]uint32) (bool, subaccountstypes.UpdateResult, subaccountstypes.UpdateResult, *big.Int, error) { - ret := _m.Called(ctx, matchWithOrders, affiliatesWhitelistMap) +func (_m *ClobKeeper) ProcessSingleMatch(ctx types.Context, matchWithOrders *clobtypes.MatchWithOrders, affiliateOverrides map[string]bool, affiliateParameters affiliatetypes.AffiliateParameters) (bool, subaccountstypes.UpdateResult, subaccountstypes.UpdateResult, *big.Int, error) { + ret := _m.Called(ctx, matchWithOrders, affiliateOverrides, affiliateParameters) if len(ret) == 0 { panic("no return value specified for ProcessSingleMatch") @@ -992,37 +993,37 @@ func (_m *ClobKeeper) ProcessSingleMatch(ctx types.Context, matchWithOrders *clo var r2 subaccountstypes.UpdateResult var r3 *big.Int var r4 error - if rf, ok := ret.Get(0).(func(types.Context, *clobtypes.MatchWithOrders, map[string]uint32) (bool, subaccountstypes.UpdateResult, subaccountstypes.UpdateResult, *big.Int, error)); ok { - return rf(ctx, matchWithOrders, affiliatesWhitelistMap) + if rf, ok := ret.Get(0).(func(types.Context, *clobtypes.MatchWithOrders, map[string]bool, affiliatetypes.AffiliateParameters) (bool, subaccountstypes.UpdateResult, subaccountstypes.UpdateResult, *big.Int, error)); ok { + return rf(ctx, matchWithOrders, affiliateOverrides, affiliateParameters) } - if rf, ok := ret.Get(0).(func(types.Context, *clobtypes.MatchWithOrders, map[string]uint32) bool); ok { - r0 = rf(ctx, matchWithOrders, affiliatesWhitelistMap) + if rf, ok := ret.Get(0).(func(types.Context, *clobtypes.MatchWithOrders, map[string]bool, affiliatetypes.AffiliateParameters) bool); ok { + r0 = rf(ctx, matchWithOrders, affiliateOverrides, affiliateParameters) } else { r0 = ret.Get(0).(bool) } - if rf, ok := ret.Get(1).(func(types.Context, *clobtypes.MatchWithOrders, map[string]uint32) subaccountstypes.UpdateResult); ok { - r1 = rf(ctx, matchWithOrders, affiliatesWhitelistMap) + if rf, ok := ret.Get(1).(func(types.Context, *clobtypes.MatchWithOrders, map[string]bool, affiliatetypes.AffiliateParameters) subaccountstypes.UpdateResult); ok { + r1 = rf(ctx, matchWithOrders, affiliateOverrides, affiliateParameters) } else { r1 = ret.Get(1).(subaccountstypes.UpdateResult) } - if rf, ok := ret.Get(2).(func(types.Context, *clobtypes.MatchWithOrders, map[string]uint32) subaccountstypes.UpdateResult); ok { - r2 = rf(ctx, matchWithOrders, affiliatesWhitelistMap) + if rf, ok := ret.Get(2).(func(types.Context, *clobtypes.MatchWithOrders, map[string]bool, affiliatetypes.AffiliateParameters) subaccountstypes.UpdateResult); ok { + r2 = rf(ctx, matchWithOrders, affiliateOverrides, affiliateParameters) } else { r2 = ret.Get(2).(subaccountstypes.UpdateResult) } - if rf, ok := ret.Get(3).(func(types.Context, *clobtypes.MatchWithOrders, map[string]uint32) *big.Int); ok { - r3 = rf(ctx, matchWithOrders, affiliatesWhitelistMap) + if rf, ok := ret.Get(3).(func(types.Context, *clobtypes.MatchWithOrders, map[string]bool, affiliatetypes.AffiliateParameters) *big.Int); ok { + r3 = rf(ctx, matchWithOrders, affiliateOverrides, affiliateParameters) } else { if ret.Get(3) != nil { r3 = ret.Get(3).(*big.Int) } } - if rf, ok := ret.Get(4).(func(types.Context, *clobtypes.MatchWithOrders, map[string]uint32) error); ok { - r4 = rf(ctx, matchWithOrders, affiliatesWhitelistMap) + if rf, ok := ret.Get(4).(func(types.Context, *clobtypes.MatchWithOrders, map[string]bool, affiliatetypes.AffiliateParameters) error); ok { + r4 = rf(ctx, matchWithOrders, affiliateOverrides, affiliateParameters) } else { r4 = ret.Error(4) } diff --git a/protocol/mocks/MemClobKeeper.go b/protocol/mocks/MemClobKeeper.go index c42066c997d..12d7bb7f791 100644 --- a/protocol/mocks/MemClobKeeper.go +++ b/protocol/mocks/MemClobKeeper.go @@ -12,6 +12,7 @@ import ( mock "github.com/stretchr/testify/mock" + affiliatetypes "github.com/dydxprotocol/v4-chain/protocol/x/affiliates/types" subaccountstypes "github.com/dydxprotocol/v4-chain/protocol/x/subaccounts/types" types "github.com/cosmos/cosmos-sdk/types" @@ -339,8 +340,8 @@ func (_m *MemClobKeeper) OffsetSubaccountPerpetualPosition(ctx types.Context, li } // ProcessSingleMatch provides a mock function with given fields: ctx, matchWithOrders, affiliatesWhitelistMap -func (_m *MemClobKeeper) ProcessSingleMatch(ctx types.Context, matchWithOrders *clobtypes.MatchWithOrders, affiliatesWhitelistMap map[string]uint32) (bool, subaccountstypes.UpdateResult, subaccountstypes.UpdateResult, *big.Int, error) { - ret := _m.Called(ctx, matchWithOrders, affiliatesWhitelistMap) +func (_m *MemClobKeeper) ProcessSingleMatch(ctx types.Context, matchWithOrders *clobtypes.MatchWithOrders, affiliateOverrides map[string]bool, affiliateParameters affiliatetypes.AffiliateParameters) (bool, subaccountstypes.UpdateResult, subaccountstypes.UpdateResult, *big.Int, error) { + ret := _m.Called(ctx, matchWithOrders, affiliateOverrides, affiliateParameters) if len(ret) == 0 { panic("no return value specified for ProcessSingleMatch") @@ -351,37 +352,37 @@ func (_m *MemClobKeeper) ProcessSingleMatch(ctx types.Context, matchWithOrders * var r2 subaccountstypes.UpdateResult var r3 *big.Int var r4 error - if rf, ok := ret.Get(0).(func(types.Context, *clobtypes.MatchWithOrders, map[string]uint32) (bool, subaccountstypes.UpdateResult, subaccountstypes.UpdateResult, *big.Int, error)); ok { - return rf(ctx, matchWithOrders, affiliatesWhitelistMap) + if rf, ok := ret.Get(0).(func(types.Context, *clobtypes.MatchWithOrders, map[string]bool, affiliatetypes.AffiliateParameters) (bool, subaccountstypes.UpdateResult, subaccountstypes.UpdateResult, *big.Int, error)); ok { + return rf(ctx, matchWithOrders, affiliateOverrides, affiliateParameters) } - if rf, ok := ret.Get(0).(func(types.Context, *clobtypes.MatchWithOrders, map[string]uint32) bool); ok { - r0 = rf(ctx, matchWithOrders, affiliatesWhitelistMap) + if rf, ok := ret.Get(0).(func(types.Context, *clobtypes.MatchWithOrders, map[string]bool, affiliatetypes.AffiliateParameters) bool); ok { + r0 = rf(ctx, matchWithOrders, affiliateOverrides, affiliateParameters) } else { r0 = ret.Get(0).(bool) } - if rf, ok := ret.Get(1).(func(types.Context, *clobtypes.MatchWithOrders, map[string]uint32) subaccountstypes.UpdateResult); ok { - r1 = rf(ctx, matchWithOrders, affiliatesWhitelistMap) + if rf, ok := ret.Get(1).(func(types.Context, *clobtypes.MatchWithOrders, map[string]bool, affiliatetypes.AffiliateParameters) subaccountstypes.UpdateResult); ok { + r1 = rf(ctx, matchWithOrders, affiliateOverrides, affiliateParameters) } else { r1 = ret.Get(1).(subaccountstypes.UpdateResult) } - if rf, ok := ret.Get(2).(func(types.Context, *clobtypes.MatchWithOrders, map[string]uint32) subaccountstypes.UpdateResult); ok { - r2 = rf(ctx, matchWithOrders, affiliatesWhitelistMap) + if rf, ok := ret.Get(2).(func(types.Context, *clobtypes.MatchWithOrders, map[string]bool, affiliatetypes.AffiliateParameters) subaccountstypes.UpdateResult); ok { + r2 = rf(ctx, matchWithOrders, affiliateOverrides, affiliateParameters) } else { r2 = ret.Get(2).(subaccountstypes.UpdateResult) } - if rf, ok := ret.Get(3).(func(types.Context, *clobtypes.MatchWithOrders, map[string]uint32) *big.Int); ok { - r3 = rf(ctx, matchWithOrders, affiliatesWhitelistMap) + if rf, ok := ret.Get(3).(func(types.Context, *clobtypes.MatchWithOrders, map[string]bool, affiliatetypes.AffiliateParameters) *big.Int); ok { + r3 = rf(ctx, matchWithOrders, affiliateOverrides, affiliateParameters) } else { if ret.Get(3) != nil { r3 = ret.Get(3).(*big.Int) } } - if rf, ok := ret.Get(4).(func(types.Context, *clobtypes.MatchWithOrders, map[string]uint32) error); ok { - r4 = rf(ctx, matchWithOrders, affiliatesWhitelistMap) + if rf, ok := ret.Get(4).(func(types.Context, *clobtypes.MatchWithOrders, map[string]bool, affiliatetypes.AffiliateParameters) error); ok { + r4 = rf(ctx, matchWithOrders, affiliateOverrides, affiliateParameters) } else { r4 = ret.Error(4) } diff --git a/protocol/scripts/affiliates/update_affiliate_parameters.py b/protocol/scripts/affiliates/update_affiliate_parameters.py new file mode 100644 index 00000000000..f2e2dc865a1 --- /dev/null +++ b/protocol/scripts/affiliates/update_affiliate_parameters.py @@ -0,0 +1,209 @@ +#!/usr/bin/env python3 +""" +Script to add an order router rev share to the protocol. +""" + +import argparse +import os +import shutil +import subprocess +import sys +import tempfile +import yaml +import json +import time +import toml +from pathlib import Path +from typing import Dict, Any + +# Mainnet configuration +mainnet_node = "https://dydx-ops-rpc.kingnodes.com:443" +mainnet_chain = "dydx-mainnet-1" + +# Staging configuration +staging_node = "https://validator.v4staging.dydx.exchange:443" +staging_chain = "dydxprotocol-testnet" + +# Testnet configuration +testnet_node = "https://validator.v4testnet.dydx.exchange:443" +testnet_chain = "dydxprotocol-testnet" + +PROPOSAL_STATUS_PASSED = 3 + +def vote_for(node, chain, proposal_id, person): + print("voting as " + person) + cmd = [ + "dydxprotocold", + "tx", + "gov", + "vote", + proposal_id, + "yes", + "--from=" + person, + "--node=" + node, + "--chain-id=" + chain, + "--keyring-backend=test", + "--fees=5000000000000000adv4tnt", + "--yes" + ] + result = subprocess.run(cmd, capture_output=True, text=True) + if result.returncode != 0: + raise Exception(f"Failed to vote: {result.stderr}") + +def load_client_config() -> Dict[str, Any]: + """ + Loads configuration from ~/.dydxprotocol/config/client.toml if it exists. + + Returns: + Dictionary containing chain-id and node from client.toml, or empty dict if not found + """ + config_path = Path.home() / ".dydxprotocol" / "config" / "client.toml" + if config_path.exists(): + try: + with open(config_path, 'r') as f: + config = toml.load(f) + return config + except Exception as e: + print(f"Warning: Could not load client.toml: {e}") + return {} + return {} + +def load_yml(file_path) -> Dict[str, Any]: + """ + Loads any yml file and returns the data as a dictionary. + + Args: + file_path: Path to the yml file + + Returns: + Dictionary containing the parsed data + """ + try: + with open(file_path, 'r', encoding='utf-8') as file: + data = yaml.safe_load(file) + return data + except FileNotFoundError: + print(f"Error: File '{file_path}' not found.") + return {} + except yaml.YAMLError as e: + print(f"Error parsing YAML file: {e}") + return {} + +def get_proposal_id(node, chain): + cmd = [ + "dydxprotocold", + "query", + "gov", + "proposals", + "--node=" + node, + "--chain-id=" + chain + ] + with tempfile.NamedTemporaryFile(mode='w', suffix='.json') as tmp_file: + subprocess.run(cmd, stdout=tmp_file) + result = load_yml(tmp_file.name) + return result['proposals'][-1]['id'] + +def main(): + # Load configuration from client.toml if available + client_config = load_client_config() + default_chain_id = client_config.get('chain-id', staging_chain) + default_node = client_config.get('node', staging_node) + + parser = argparse.ArgumentParser(description='Update affiliate parameters') + parser.add_argument('--chain-id', default=default_chain_id, help=f'Chain ID, default from client.toml or {staging_chain}') + parser.add_argument('--node', default=default_node, help=f'Node URL, default from client.toml or {staging_node}') + parser.add_argument('--max-30d-commission', type=int, required=True, help='Maximum 30d commission per referred') + parser.add_argument('--referee-min-fee-tier', type=int, required=True, help='Referee minimum fee tier idx') + parser.add_argument('--max-30d-volume', type=int, required=True, help='Maximum 30d attributable volume per referred') + args = parser.parse_args() + + # Print configuration source + if client_config: + print(f"Loaded configuration from ~/.dydxprotocol/config/client.toml") + print(f"Using chain-id: {args.chain_id}") + print(f"Using node: {args.node}") + + counter = 0 + # 3 retries for the process. + for i in range(3): + try: + with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as tmp_file: + affiliate_parameters_msg = { + "messages": [ + { + "@type": "/dydxprotocol.affiliates.MsgUpdateAffiliateParameters", + "authority": "dydx10d07y265gmmuvt4z0w9aw880jnsr700jnmapky", + "affiliate_parameters": { + "maximum_30d_attributable_volume_per_referred_user_notional": int(args.max_30d_volume), + "referee_minimum_fee_tier_idx": int(args.referee_min_fee_tier), + "maximum_30d_attributable_revenue_per_referred_user_quote_quantums": int(args.max_30d_commission), + } + } + ], + "deposit": "10000000000000000000000adv4tnt", + "metadata": "", + "title": "Update affiliate parameters", + "summary": f"Update affiliate parameters: max_30d_commission={args.max_30d_commission}, referee_min_fee_tier={args.referee_min_fee_tier}, max_30d_volume={args.max_30d_volume}" + } + json.dump(affiliate_parameters_msg, tmp_file, indent=2) + print(affiliate_parameters_msg) + tmp_file_path = tmp_file.name + print("submitting proposal for affiliate parameters update") + cmd = [ + "dydxprotocold", + "tx", + "gov", + "submit-proposal", + tmp_file_path, + "--from=alice", + "--gas=auto", + "--fees=10000000000000000000000adv4tnt", + "--node=" + args.node, + "--chain-id=" + args.chain_id, + "--keyring-backend=test", + "--yes" + ] + + # Print the full command + result = subprocess.run(cmd, capture_output=True, text=True) + if result.returncode != 0: + raise Exception(f"Failed to submit proposal: {result.stderr}") + # delete the temporary file + os.remove(tmp_file_path) + print("voting for affiliate parameters update") + time.sleep(5) + # vote for alice + voters = ["alice", "bob", "carl", "dave", "emily", "fiona", "greg", "henry", "ian", "jeff"] + proposal_id = get_proposal_id(args.node, args.chain_id) + for voter in voters: + vote_for(args.node, args.chain_id, proposal_id, voter) + + # wait for the proposal to pass + print("Waiting 2 minutes for proposal to pass") + time.sleep(120) + # check if the proposal passed + cmd = [ + "/Users/justinbarnett/projects/v4-chain/protocol/build/dydxprotocold", + "query", + "gov", + "proposal", + proposal_id, + "--node=" + args.node, + "--chain-id=" + args.chain_id + ] + with tempfile.NamedTemporaryFile(mode='w', suffix='.json') as tmp_file: + subprocess.run(cmd, stdout=tmp_file) + result = load_yml(tmp_file.name) + if result['proposal']['status'] == PROPOSAL_STATUS_PASSED: + print("proposal passed, affiliate parameters updated") + return True + else: + raise Exception("Failed to update affiliate parameters") + break + except Exception as e: + print(e) + print(f"got exception, retrying {i+1} time(s)") + + +if __name__ == "__main__": + main() diff --git a/protocol/scripts/genesis/sample_pregenesis.json b/protocol/scripts/genesis/sample_pregenesis.json index 6e78202357d..d668fe4e731 100644 --- a/protocol/scripts/genesis/sample_pregenesis.json +++ b/protocol/scripts/genesis/sample_pregenesis.json @@ -24,6 +24,14 @@ "req_referred_volume_quote_quantums": "25000000000000", "req_staked_whole_coins": 5000, "taker_fee_share_ppm": 150000 +<<<<<<< HEAD +======= + }, + { + "req_referred_volume_quote_quantums": "50000000000000", + "req_staked_whole_coins": 100000000, + "taker_fee_share_ppm": 250000 +>>>>>>> c29eea29 (Dont attribute new revenue if user exceeds 30d max volume and deprecate AffiliateWhitelist (#3109)) } ] } diff --git a/protocol/testing/testnet/genesis.json b/protocol/testing/testnet/genesis.json index 3fe44a2f9b8..71cd53cb8f9 100644 --- a/protocol/testing/testnet/genesis.json +++ b/protocol/testing/testnet/genesis.json @@ -36,6 +36,13 @@ } ] }, + "affiliates": { + "affiliate_parameters": { + "maximum_30d_attributable_volume_per_referred_user_notional": "100000000000", + "referee_minimum_fee_tier_idx": 2, + "maximum_30d_attributable_revenue_per_referred_user_quote_quantums": "10000000000" + } + }, "auth": { "params": { "max_memo_characters": "256", diff --git a/protocol/testutil/memclob/keeper.go b/protocol/testutil/memclob/keeper.go index 3b4282e42ea..98da5824adb 100644 --- a/protocol/testutil/memclob/keeper.go +++ b/protocol/testutil/memclob/keeper.go @@ -8,6 +8,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/dydxprotocol/v4-chain/protocol/indexer/indexer_manager" "github.com/dydxprotocol/v4-chain/protocol/testutil/constants" + affiliatetypes "github.com/dydxprotocol/v4-chain/protocol/x/affiliates/types" "github.com/dydxprotocol/v4-chain/protocol/x/clob/types" satypes "github.com/dydxprotocol/v4-chain/protocol/x/subaccounts/types" ) @@ -327,7 +328,8 @@ func (f *FakeMemClobKeeper) addFakeFillAmount( func (f *FakeMemClobKeeper) ProcessSingleMatch( ctx sdk.Context, matchWithOrders *types.MatchWithOrders, - affiliatesWhitelistMap map[string]uint32, + affiliateOverrides map[string]bool, + affiliateParameters affiliatetypes.AffiliateParameters, ) ( success bool, takerUpdateResult satypes.UpdateResult, diff --git a/protocol/x/affiliates/keeper/grpc_query.go b/protocol/x/affiliates/keeper/grpc_query.go index 84185382a8e..1d82e083277 100644 --- a/protocol/x/affiliates/keeper/grpc_query.go +++ b/protocol/x/affiliates/keeper/grpc_query.go @@ -22,21 +22,19 @@ func (k Keeper) AffiliateInfo(c context.Context, req.GetAddress(), err.Error()) } - affiliateWhitelistMap, err := k.GetAffiliateWhitelistMap(ctx) + affiliateOverridesMap, err := k.GetAffiliateOverridesMap(ctx) if err != nil { return nil, err } tierLevel := uint32(0) feeSharePpm := uint32(0) isWhitelisted := false - if _, exists := affiliateWhitelistMap[addr.String()]; exists { - feeSharePpm = affiliateWhitelistMap[addr.String()] + if _, exists := affiliateOverridesMap[addr.String()]; exists { isWhitelisted = true - } else { - tierLevel, feeSharePpm, err = k.GetTierForAffiliate(ctx, addr.String()) - if err != nil { - return nil, err - } + } + tierLevel, feeSharePpm, err = k.GetTierForAffiliate(ctx, addr.String(), affiliateOverridesMap) + if err != nil { + return nil, err } referredVolume, err := k.GetReferredVolume(ctx, req.GetAddress()) diff --git a/protocol/x/affiliates/keeper/grpc_query_test.go b/protocol/x/affiliates/keeper/grpc_query_test.go index 23f0e35c936..f0073d3925c 100644 --- a/protocol/x/affiliates/keeper/grpc_query_test.go +++ b/protocol/x/affiliates/keeper/grpc_query_test.go @@ -97,8 +97,8 @@ func TestAffiliateInfo(t *testing.T) { }, res: &types.AffiliateInfoResponse{ IsWhitelisted: true, - Tier: 0, - FeeSharePpm: 120_000, + Tier: 4, + FeeSharePpm: 250_000, ReferredVolume: dtypes.NewIntFromUint64(0), StakedAmount: dtypes.NewIntFromUint64(0), }, @@ -117,15 +117,10 @@ func TestAffiliateInfo(t *testing.T) { ) require.NoError(t, err) - affiliatesWhitelist := types.AffiliateWhitelist{ - Tiers: []types.AffiliateWhitelist_Tier{ - { - Addresses: []string{constants.AliceAccAddress.String()}, - TakerFeeSharePpm: 120_000, // 12% - }, - }, + affiliateOverrides := types.AffiliateOverrides{ + Addresses: []string{constants.AliceAccAddress.String()}, } - err = k.SetAffiliateWhitelist(ctx, affiliatesWhitelist) + err = k.SetAffiliateOverrides(ctx, affiliateOverrides) require.NoError(t, err) }, }, diff --git a/protocol/x/affiliates/keeper/keeper.go b/protocol/x/affiliates/keeper/keeper.go index 52cb1bfd3a6..3892ac9091d 100644 --- a/protocol/x/affiliates/keeper/keeper.go +++ b/protocol/x/affiliates/keeper/keeper.go @@ -15,6 +15,7 @@ import ( "github.com/dydxprotocol/v4-chain/protocol/indexer/indexer_manager" "github.com/dydxprotocol/v4-chain/protocol/lib" "github.com/dydxprotocol/v4-chain/protocol/x/affiliates/types" + statstypes "github.com/dydxprotocol/v4-chain/protocol/x/stats/types" ) type ( @@ -109,13 +110,32 @@ func (k Keeper) GetReferredBy(ctx sdk.Context, referee string) (string, bool) { return string(referredByPrefixStore.Get([]byte(referee))), true } -// AddReferredVolume adds the referred volume from a block to the affiliate's referred volume. +func (k Keeper) SetReferredVolume( + ctx sdk.Context, + referrer string, + referredVolume *big.Int, +) error { + affiliateReferredVolumePrefixStore := prefix.NewStore(ctx.KVStore(k.storeKey), + []byte(types.ReferredVolumeInWindowKeyPrefix)) + updatedReferedVolume := dtypes.NewIntFromBigInt(referredVolume) + + updatedReferredVolumeBytes, err := updatedReferedVolume.Marshal() + if err != nil { + return errorsmod.Wrapf(types.ErrUpdatingAffiliateReferredVolume, + "referrer %s, error: %s", referrer, err) + } + affiliateReferredVolumePrefixStore.Set([]byte(referrer), updatedReferredVolumeBytes) + return nil +} + +// AddReferredVolume adds the referred volume from a block to the affiliate's referred volume in the window. func (k Keeper) AddReferredVolume( ctx sdk.Context, affiliateAddr string, referredVolumeFromBlock *big.Int, ) error { - affiliateReferredVolumePrefixStore := prefix.NewStore(ctx.KVStore(k.storeKey), []byte(types.ReferredVolumeKeyPrefix)) + affiliateReferredVolumePrefixStore := prefix.NewStore(ctx.KVStore(k.storeKey), + []byte(types.ReferredVolumeInWindowKeyPrefix)) referredVolume := big.NewInt(0) if affiliateReferredVolumePrefixStore.Has([]byte(affiliateAddr)) { @@ -133,6 +153,10 @@ func (k Keeper) AddReferredVolume( referredVolume, referredVolumeFromBlock, ) + + if referredVolume.Cmp(big.NewInt(0)) < 0 { + referredVolume = big.NewInt(0) + } updatedReferedVolume := dtypes.NewIntFromBigInt(referredVolume) updatedReferredVolumeBytes, err := updatedReferedVolume.Marshal() @@ -144,9 +168,10 @@ func (k Keeper) AddReferredVolume( return nil } -// GetReferredVolume returns all time referred volume for an affiliate address. +// GetReferredVolume returns all time referred volume for an affiliate address in the window. func (k Keeper) GetReferredVolume(ctx sdk.Context, affiliateAddr string) (*big.Int, error) { - affiliateReferredVolumePrefixStore := prefix.NewStore(ctx.KVStore(k.storeKey), []byte(types.ReferredVolumeKeyPrefix)) + affiliateReferredVolumePrefixStore := prefix.NewStore(ctx.KVStore(k.storeKey), + []byte(types.ReferredVolumeInWindowKeyPrefix)) if !affiliateReferredVolumePrefixStore.Has([]byte(affiliateAddr)) { return big.NewInt(0), nil } @@ -180,7 +205,7 @@ func (k Keeper) GetAllAffiliateTiers(ctx sdk.Context) (types.AffiliateTiers, err func (k Keeper) GetTakerFeeShare( ctx sdk.Context, address string, - affiliatesWhitelistMap map[string]uint32, + affiliateOverrides map[string]bool, ) ( affiliateAddress string, feeSharePpm uint32, @@ -191,13 +216,7 @@ func (k Keeper) GetTakerFeeShare( if !exists { return "", 0, false, nil } - // Override fee share ppm if the address is in the whitelist. - if _, exists := affiliatesWhitelistMap[affiliateAddress]; exists { - feeSharePpm = affiliatesWhitelistMap[affiliateAddress] - return affiliateAddress, feeSharePpm, true, nil - } - - _, feeSharePpm, err = k.GetTierForAffiliate(ctx, affiliateAddress) + _, feeSharePpm, err = k.GetTierForAffiliate(ctx, affiliateAddress, affiliateOverrides) if err != nil { return "", 0, false, err } @@ -209,6 +228,7 @@ func (k Keeper) GetTakerFeeShare( func (k Keeper) GetTierForAffiliate( ctx sdk.Context, affiliateAddr string, + affiliateOverrides map[string]bool, ) ( tierLevel uint32, feeSharePpm uint32, @@ -225,6 +245,20 @@ func (k Keeper) GetTierForAffiliate( numTiers := uint32(len(tiers)) maxTierLevel := numTiers - 1 currentTier := uint32(0) +<<<<<<< HEAD +======= + + // Check whether the address is overridden, if it is then set the + // affiliate tier to the max + if affiliateOverrides != nil { + if _, exists := affiliateOverrides[affiliateAddr]; exists { + feeSharePpm = affiliateTiers.Tiers[maxTierLevel].TakerFeeSharePpm + return uint32(maxTierLevel), feeSharePpm, nil + } + } + + // If not then set it normally +>>>>>>> c29eea29 (Dont attribute new revenue if user exceeds 30d max volume and deprecate AffiliateWhitelist (#3109)) referredVolume, err := k.GetReferredVolume(ctx, affiliateAddr) if err != nil { return 0, 0, err @@ -350,12 +384,122 @@ func (k Keeper) GetAffiliateWhitelist(ctx sdk.Context) (types.AffiliateWhitelist return affiliateWhitelist, nil } +<<<<<<< HEAD +======= +func (k Keeper) UpdateAffiliateParameters( + ctx sdk.Context, + msg *types.MsgUpdateAffiliateParameters, +) error { + store := ctx.KVStore(k.storeKey) + + affiliateParametersBytes, err := k.cdc.Marshal(&msg.AffiliateParameters) + if err != nil { + return err + } + store.Set([]byte(types.AffiliateParametersKey), affiliateParametersBytes) + + return nil +} + +func (k Keeper) GetAffiliateParameters(ctx sdk.Context) (types.AffiliateParameters, error) { + store := ctx.KVStore(k.storeKey) + affiliateParametersBytes := store.Get([]byte(types.AffiliateParametersKey)) + if affiliateParametersBytes == nil { + return types.AffiliateParameters{}, nil + } + affiliateParameters := types.AffiliateParameters{} + err := k.cdc.Unmarshal(affiliateParametersBytes, &affiliateParameters) + if err != nil { + return types.AffiliateParameters{}, err + } + return affiliateParameters, nil +} + +func (k Keeper) SetAffiliateOverrides(ctx sdk.Context, overrides types.AffiliateOverrides) error { + store := ctx.KVStore(k.storeKey) + affiliateOverridesBytes, err := k.cdc.Marshal(&overrides) + if err != nil { + return err + } + store.Set([]byte(types.AffiliateOverridesKey), affiliateOverridesBytes) + return nil +} + +func (k Keeper) GetAffiliateOverrides(ctx sdk.Context) (types.AffiliateOverrides, error) { + store := ctx.KVStore(k.storeKey) + affiliateOverridesBytes := store.Get([]byte(types.AffiliateOverridesKey)) + if affiliateOverridesBytes == nil { + return types.AffiliateOverrides{}, nil + } + affiliateOverrides := types.AffiliateOverrides{} + err := k.cdc.Unmarshal(affiliateOverridesBytes, &affiliateOverrides) + if err != nil { + return types.AffiliateOverrides{}, err + } + return affiliateOverrides, nil +} + +func (k Keeper) GetAffiliateOverridesMap(ctx sdk.Context) (map[string]bool, error) { + affiliateOverrides, err := k.GetAffiliateOverrides(ctx) + if err != nil { + return nil, err + } + affiliateOverridesMap := make(map[string]bool) + for _, address := range affiliateOverrides.Addresses { + affiliateOverridesMap[address] = true + } + 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 parameter is 0 then no limit is applied + previousVolume := (refereeUserStats.TakerNotional + refereeUserStats.MakerNotional + + previouslyAttributedVolume[referee]) + + cap := affiliateParams.Maximum_30DAttributableVolumePerReferredUserNotional + 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 { + if err := k.AddReferredVolume(ctx, referrer, lib.BigU(volume)); err != nil { + return err + } + } + return nil +} + +>>>>>>> c29eea29 (Dont attribute new revenue if user exceeds 30d max volume and deprecate AffiliateWhitelist (#3109)) 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] @@ -367,7 +511,15 @@ func (k Keeper) AggregateAffiliateReferredVolumeForFills( } } if referredByAddrTaker != "" { - if err := k.AddReferredVolume(ctx, referredByAddrTaker, lib.BigU(fill.Notional)); err != nil { + // 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 } } @@ -382,10 +534,62 @@ func (k Keeper) AggregateAffiliateReferredVolumeForFills( } } if referredByAddrMaker != "" { - if err := k.AddReferredVolume(ctx, referredByAddrMaker, lib.BigU(fill.Notional)); err != nil { + if err := k.addReferredVolumeIfQualified( + ctx, + fill.Maker, + referredByAddrMaker, + fill.Notional, + affiliateParams, + previouslyAttributedVolume, + ); err != nil { return err } } } return nil } + +// OnStatsExpired implements StatsExpirationHook interface +// Called when a user's stats expire from the 30d rolling window, update the +// users referred volume to reflect the expired volume. +func (k Keeper) OnStatsExpired( + ctx sdk.Context, + userAddress string, + resultingUserStats *statstypes.UserStats, +) error { + // Get affiliate parameters + affiliateParams, err := k.GetAffiliateParameters(ctx) + if err != nil { + return err + } + + // Check if this user has a referrer (is a referee) + referrer, found := k.GetReferredBy(ctx, userAddress) + if !found { + return nil // User is not referred, nothing to do + } + + resultingVolume := resultingUserStats.TakerNotional + resultingUserStats.MakerNotional + var deltaAttributedVolume uint64 + if resultingVolume < affiliateParams.Maximum_30DAttributableVolumePerReferredUserNotional { + deltaAttributedVolume = affiliateParams.Maximum_30DAttributableVolumePerReferredUserNotional - resultingVolume + } + + // Get current referred volume for the referrer + currentVolume, err := k.GetReferredVolume(ctx, referrer) + if err != nil { + return err + } + + // Subtract the expired volume (use taker volume for consistency with how it's added) + expiredVolume := lib.BigU(deltaAttributedVolume) + newVolume := new(big.Int).Sub(currentVolume, expiredVolume) + + // Ensure it doesn't go negative + if newVolume.Cmp(big.NewInt(0)) < 0 { + newVolume = big.NewInt(0) + } + + // Update the referred volume + return k.SetReferredVolume(ctx, referrer, newVolume) +} diff --git a/protocol/x/affiliates/keeper/keeper_test.go b/protocol/x/affiliates/keeper/keeper_test.go index 5a889d1f9de..01f39ac7e54 100644 --- a/protocol/x/affiliates/keeper/keeper_test.go +++ b/protocol/x/affiliates/keeper/keeper_test.go @@ -150,7 +150,7 @@ func TestAddReferredVolume(t *testing.T) { updatedVolume, err := k.GetReferredVolume(ctx, affiliate) require.NoError(t, err) - require.Equal(t, big.NewInt(1500), updatedVolume) + require.Equal(t, initialVolume.Add(initialVolume, addedVolume), updatedVolume) } func TestGetReferredVolumeInvalidAffiliate(t *testing.T) { @@ -196,7 +196,7 @@ func TestGetTakerFeeShareViaReferredVolume(t *testing.T) { require.NoError(t, err) // Get taker fee share for referee - affiliateAddr, feeSharePpm, exists, err := k.GetTakerFeeShare(ctx, referee, map[string]uint32{}) + affiliateAddr, feeSharePpm, exists, err := k.GetTakerFeeShare(ctx, referee, map[string]bool{}) require.NoError(t, err) require.True(t, exists) require.Equal(t, affiliate, affiliateAddr) @@ -209,7 +209,7 @@ func TestGetTakerFeeShareViaReferredVolume(t *testing.T) { require.NoError(t, err) // Get updated taker fee share for referee - affiliateAddr, feeSharePpm, exists, err = k.GetTakerFeeShare(ctx, referee, map[string]uint32{}) + affiliateAddr, feeSharePpm, exists, err = k.GetTakerFeeShare(ctx, referee, map[string]bool{}) require.NoError(t, err) require.True(t, exists) require.Equal(t, affiliate, affiliateAddr) @@ -245,7 +245,7 @@ func TestGetTakerFeeShareViaStakedAmount(t *testing.T) { require.NoError(t, err) // Get taker fee share for referee - affiliateAddr, feeSharePpm, exists, err := k.GetTakerFeeShare(ctx, referee, map[string]uint32{}) + affiliateAddr, feeSharePpm, exists, err := k.GetTakerFeeShare(ctx, referee, map[string]bool{}) require.NoError(t, err) require.True(t, exists) require.Equal(t, affiliate, affiliateAddr) @@ -263,7 +263,7 @@ func TestGetTakerFeeShareViaStakedAmount(t *testing.T) { )))) require.NoError(t, err) // Get updated taker fee share for referee - affiliateAddr, feeSharePpm, exists, err = k.GetTakerFeeShare(ctx, referee, map[string]uint32{}) + affiliateAddr, feeSharePpm, exists, err = k.GetTakerFeeShare(ctx, referee, map[string]bool{}) require.NoError(t, err) require.True(t, exists) require.Equal(t, affiliate, affiliateAddr) @@ -315,7 +315,7 @@ func TestGetTierForAffiliate_VolumeAndStake(t *testing.T) { ) require.NoError(t, err) - tierLevel, feeSharePpm, err := k.GetTierForAffiliate(ctx, affiliate) + tierLevel, feeSharePpm, err := k.GetTierForAffiliate(ctx, affiliate, map[string]bool{}) require.NoError(t, err) require.Equal(t, uint32(3), tierLevel) @@ -624,7 +624,7 @@ func TestGetTakerFeeShareViaWhitelist(t *testing.T) { name string affiliateAddr string refereeAddr string - whitelist *types.AffiliateWhitelist + overrides *types.AffiliateOverrides expectedFeeSharePpm uint32 expectedExists bool }{ @@ -632,22 +632,17 @@ func TestGetTakerFeeShareViaWhitelist(t *testing.T) { name: "Affiliate in whitelist", affiliateAddr: constants.AliceAccAddress.String(), refereeAddr: constants.BobAccAddress.String(), - whitelist: &types.AffiliateWhitelist{ - Tiers: []types.AffiliateWhitelist_Tier{ - { - Addresses: []string{constants.AliceAccAddress.String()}, - TakerFeeSharePpm: 400_000, // 40% - }, - }, + overrides: &types.AffiliateOverrides{ + Addresses: []string{constants.AliceAccAddress.String()}, }, - expectedFeeSharePpm: 400_000, // 40% + expectedFeeSharePpm: 250_000, // 25% expectedExists: true, }, { name: "Affiliate not in whitelist", affiliateAddr: constants.AliceAccAddress.String(), refereeAddr: constants.BobAccAddress.String(), - whitelist: &types.AffiliateWhitelist{}, + overrides: &types.AffiliateOverrides{}, expectedFeeSharePpm: tiers.Tiers[0].TakerFeeSharePpm, expectedExists: true, }, @@ -655,13 +650,8 @@ func TestGetTakerFeeShareViaWhitelist(t *testing.T) { name: "Referee not registered", affiliateAddr: "", refereeAddr: constants.BobAccAddress.String(), - whitelist: &types.AffiliateWhitelist{ - Tiers: []types.AffiliateWhitelist_Tier{ - { - Addresses: []string{constants.AliceAccAddress.String()}, - TakerFeeSharePpm: 400_000, // 40% - }, - }, + overrides: &types.AffiliateOverrides{ + Addresses: []string{constants.AliceAccAddress.String()}, }, expectedFeeSharePpm: 0, expectedExists: false, @@ -674,18 +664,18 @@ func TestGetTakerFeeShareViaWhitelist(t *testing.T) { err := k.UpdateAffiliateTiers(ctx, tiers) require.NoError(t, err) - if tc.whitelist != nil { - err := k.SetAffiliateWhitelist(ctx, *tc.whitelist) + if tc.overrides != nil { + err := k.SetAffiliateOverrides(ctx, *tc.overrides) require.NoError(t, err) } if tc.affiliateAddr != "" { err := k.RegisterAffiliate(ctx, tc.refereeAddr, tc.affiliateAddr) require.NoError(t, err) } - affiliateWhitelistMap, err := k.GetAffiliateWhitelistMap(ctx) + affiliateOverridesMap, err := k.GetAffiliateOverridesMap(ctx) require.NoError(t, err) - affiliateAddr, feeSharePpm, exists, err := k.GetTakerFeeShare(ctx, tc.refereeAddr, affiliateWhitelistMap) + affiliateAddr, feeSharePpm, exists, err := k.GetTakerFeeShare(ctx, tc.refereeAddr, affiliateOverridesMap) require.NoError(t, err) require.Equal(t, tc.affiliateAddr, affiliateAddr) require.Equal(t, tc.expectedFeeSharePpm, feeSharePpm) @@ -700,14 +690,18 @@ func TestAggregateAffiliateReferredVolumeForFills(t *testing.T) { referee2 := constants.DaveAccAddress.String() maker := constants.CarlAccAddress.String() testCases := []struct { - name string - referrals int - expectedVolume *big.Int - setup func(t *testing.T, ctx sdk.Context, k *keeper.Keeper, statsKeeper *statskeeper.Keeper) + name string + referrals int + expectedVolume *big.Int + expectedAttributedVolume *big.Int + referreeAddressesToVerify []string + expectedCommissions []*big.Int + setup func(t *testing.T, ctx sdk.Context, k *keeper.Keeper, statsKeeper *statskeeper.Keeper) }{ { - name: "0 referrals", - expectedVolume: big.NewInt(0), + name: "0 referrals", + expectedVolume: big.NewInt(0), + expectedAttributedVolume: big.NewInt(0), setup: func(t *testing.T, ctx sdk.Context, k *keeper.Keeper, statsKeeper *statskeeper.Keeper) { statsKeeper.SetBlockStats(ctx, &statstypes.BlockStats{ Fills: []*statstypes.BlockStats_Fill{ @@ -721,27 +715,30 @@ func TestAggregateAffiliateReferredVolumeForFills(t *testing.T) { }, }, { - name: "1 referral", - referrals: 1, - expectedVolume: big.NewInt(100_000_000_000), + name: "1 referral", + referrals: 1, + expectedVolume: big.NewInt(100_000_000_000), + expectedAttributedVolume: big.NewInt(100_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) statsKeeper.SetBlockStats(ctx, &statstypes.BlockStats{ Fills: []*statstypes.BlockStats_Fill{ { - Taker: referee1, - Maker: maker, - Notional: 100_000_000_000, + Taker: referee1, + Maker: maker, + Notional: 100_000_000_000, + AffiliateFeeGeneratedQuantums: 1_000_000_000, }, }, }) }, }, { - name: "2 referrals", - referrals: 2, - expectedVolume: big.NewInt(300_000_000_000), + name: "2 referrals", + referrals: 2, + expectedVolume: big.NewInt(300_000_000_000), + expectedAttributedVolume: big.NewInt(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) @@ -750,23 +747,26 @@ func TestAggregateAffiliateReferredVolumeForFills(t *testing.T) { statsKeeper.SetBlockStats(ctx, &statstypes.BlockStats{ Fills: []*statstypes.BlockStats_Fill{ { - Taker: referee1, - Maker: maker, - Notional: 100_000_000_000, + Taker: referee1, + Maker: maker, + Notional: 100_000_000_000, + AffiliateFeeGeneratedQuantums: 1_000_000_000, }, { - Taker: referee2, - Maker: maker, - Notional: 200_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), + name: "2 referrals, maker also referred", + referrals: 2, + expectedVolume: big.NewInt(600_000_000_000), + expectedAttributedVolume: big.NewInt(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) @@ -777,40 +777,209 @@ func TestAggregateAffiliateReferredVolumeForFills(t *testing.T) { statsKeeper.SetBlockStats(ctx, &statstypes.BlockStats{ Fills: []*statstypes.BlockStats_Fill{ { - Taker: referee1, - Maker: maker, - Notional: 100_000_000_000, + Taker: referee1, + Maker: maker, + Notional: 100_000_000_000, + AffiliateFeeGeneratedQuantums: 1_000_000_000, }, { - Taker: referee2, - Maker: maker, - Notional: 200_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_30DAttributableVolumePerReferredUserNotional: 300_000_000_000, + }, + }) + require.NoError(t, err) }, }, { - name: "2 referrals, takers not referred, maker referred", - referrals: 2, - expectedVolume: big.NewInt(300_000_000_000), + name: "2 referrals, takers not referred, maker referred", + referrals: 2, + expectedVolume: big.NewInt(300_000_000_000), + expectedAttributedVolume: big.NewInt(300_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) statsKeeper.SetBlockStats(ctx, &statstypes.BlockStats{ Fills: []*statstypes.BlockStats_Fill{ { - Taker: referee1, - Maker: maker, - Notional: 100_000_000_000, + Taker: referee1, + Maker: maker, + Notional: 100_000_000_000, + AffiliateFeeGeneratedQuantums: 1_000_000_000, }, { - Taker: referee2, - Maker: maker, - Notional: 200_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_30DAttributableVolumePerReferredUserNotional: 300_000_000_000, + }, + }) + require.NoError(t, err) + }, + }, + { + name: "2 referrals, reached maximum attributable revenue", + referrals: 2, + expectedVolume: big.NewInt(300_000_000_000), + expectedAttributedVolume: big.NewInt(0), + 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) + // 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_30DAttributableVolumePerReferredUserNotional: 250_000_000_000, + }, + }) + require.NoError(t, err) + }, + }, + { + name: "2 referrals, test limits of attributable revenue", + referrals: 2, + expectedVolume: big.NewInt(300_000_000_000), + expectedAttributedVolume: big.NewInt(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, referee1, &statstypes.UserStats{ + TakerNotional: 50_000_000_000, + MakerNotional: 100_000_000_000, + AffiliateRevenueGeneratedQuantums: 1_000_000_000, + }) + statsKeeper.SetUserStats(ctx, referee2, &statstypes.UserStats{ + TakerNotional: 50_000_000_000, + MakerNotional: 100_000_000_000, + AffiliateRevenueGeneratedQuantums: 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_30DAttributableVolumePerReferredUserNotional: 250_000_000_000, + }, + }) + require.NoError(t, err) + }, + }, + { + name: "maker is also affiliate, test limits of attributable revenue", + referrals: 2, + expectedVolume: big.NewInt(600_000_000_000), + expectedAttributedVolume: big.NewInt(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, referee1, &statstypes.UserStats{ + TakerNotional: 50_000_000_000, + MakerNotional: 100_000_000_000, + AffiliateRevenueGeneratedQuantums: 1_000_000_000, + }) + statsKeeper.SetUserStats(ctx, referee2, &statstypes.UserStats{ + TakerNotional: 50_000_000_000, + MakerNotional: 100_000_000_000, + AffiliateRevenueGeneratedQuantums: 1_000_000_000, + }) + statsKeeper.SetUserStats(ctx, maker, &statstypes.UserStats{ + TakerNotional: 50_000_000_000, + MakerNotional: 50_000_000_000, + AffiliateRevenueGeneratedQuantums: 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_30DAttributableVolumePerReferredUserNotional: 250_000_000_000, + }, + }) + require.NoError(t, err) }, }, } @@ -832,7 +1001,7 @@ func TestAggregateAffiliateReferredVolumeForFills(t *testing.T) { referredVolume, err := k.GetReferredVolume(ctx, affiliate) require.NoError(t, err) - require.Equal(t, tc.expectedVolume, referredVolume) + require.Equal(t, tc.expectedAttributedVolume, referredVolume) }) } } @@ -842,8 +1011,117 @@ func TestGetTierForAffiliateEmptyTiers(t *testing.T) { ctx := tApp.InitChain() k := tApp.App.AffiliatesKeeper - tierLevel, feeSharePpm, err := k.GetTierForAffiliate(ctx, constants.AliceAccAddress.String()) + tierLevel, feeSharePpm, err := k.GetTierForAffiliate(ctx, constants.AliceAccAddress.String(), map[string]bool{}) require.NoError(t, err) require.Equal(t, uint32(0), tierLevel) require.Equal(t, uint32(0), feeSharePpm) } +<<<<<<< HEAD +======= + +func TestUpdateAffiliateParameters(t *testing.T) { + tApp := testapp.NewTestAppBuilder(t).Build() + ctx := tApp.InitChain() + k := tApp.App.AffiliatesKeeper + + err := k.UpdateAffiliateParameters(ctx, &types.MsgUpdateAffiliateParameters{ + Authority: constants.GovAuthority, + AffiliateParameters: types.DefaultAffiliateParameters, + }) + require.NoError(t, err) + + affiliateParameters, err := k.GetAffiliateParameters(ctx) + require.NoError(t, err) + require.Equal(t, uint64(100), affiliateParameters.GetMaximum_30DAttributableVolumePerReferredUserNotional()) + require.Equal(t, uint32(1), affiliateParameters.GetRefereeMinimumFeeTierIdx()) + require.Equal(t, uint64(100), affiliateParameters.GetMaximum_30DAttributableRevenuePerReferredUserQuoteQuantums()) +} + +func TestGetTierForAffiliateOverrides(t *testing.T) { + tApp := testapp.NewTestAppBuilder(t).Build() + ctx := tApp.InitChain() + k := tApp.App.AffiliatesKeeper + + err := k.UpdateAffiliateTiers(ctx, types.DefaultAffiliateTiers) + require.NoError(t, err) + + tierLevel, feeSharePpm, err := k.GetTierForAffiliate(ctx, constants.AliceAccAddress.String(), map[string]bool{}) + require.NoError(t, err) + require.Equal(t, uint32(3), tierLevel) + require.Equal(t, uint32(150_000), feeSharePpm) + + tierLevel, feeSharePpm, err = k.GetTierForAffiliate(ctx, constants.AliceAccAddress.String(), map[string]bool{ + constants.AliceAccAddress.String(): true, + }) + require.NoError(t, err) + require.Equal(t, uint32(4), tierLevel) + require.Equal(t, uint32(250_000), feeSharePpm) +} + +func TestOnStatsExpiredHook(t *testing.T) { + tApp := testapp.NewTestAppBuilder(t).Build() + ctx := tApp.InitChain() + k := tApp.App.AffiliatesKeeper + referrer := constants.AliceAccAddress.String() + referee := constants.BobAccAddress.String() + err := k.UpdateAffiliateTiers(ctx, types.DefaultAffiliateTiers) + require.NoError(t, err) + + err = k.RegisterAffiliate(ctx, referee, referrer) + require.NoError(t, err) + + err = k.UpdateAffiliateParameters(ctx, &types.MsgUpdateAffiliateParameters{ + Authority: constants.GovAuthority, + AffiliateParameters: types.DefaultAffiliateParameters, + }) + require.NoError(t, err) + + for _, tc := range []struct { + name string + initialReferredVolume *big.Int + resultingUserStats *statstypes.UserStats + expectedReferredVolume *big.Int + }{ + { + name: "referee hit maximum attributable volume", + initialReferredVolume: big.NewInt(100), + resultingUserStats: &statstypes.UserStats{ + TakerNotional: 50, + MakerNotional: 20, + AffiliateRevenueGeneratedQuantums: 100, + }, + expectedReferredVolume: big.NewInt(70), + }, + { + name: "referee started at 0 attributable volume", + initialReferredVolume: big.NewInt(0), + resultingUserStats: &statstypes.UserStats{ + TakerNotional: 0, + MakerNotional: 0, + AffiliateRevenueGeneratedQuantums: 100, + }, + expectedReferredVolume: big.NewInt(0), + }, + { + name: "normal case expired to 0", + initialReferredVolume: big.NewInt(75), + resultingUserStats: &statstypes.UserStats{ + TakerNotional: 0, + MakerNotional: 0, + AffiliateRevenueGeneratedQuantums: 100, + }, + expectedReferredVolume: big.NewInt(0), + }, + } { + err := k.SetReferredVolume(ctx, referrer, tc.initialReferredVolume) + require.NoError(t, err) + + err = k.OnStatsExpired(ctx, referee, tc.resultingUserStats) + require.NoError(t, err) + + referredVolume, err := k.GetReferredVolume(ctx, referrer) + require.NoError(t, err) + require.Equal(t, tc.expectedReferredVolume, referredVolume) + } +} +>>>>>>> c29eea29 (Dont attribute new revenue if user exceeds 30d max volume and deprecate AffiliateWhitelist (#3109)) diff --git a/protocol/x/affiliates/types/constants.go b/protocol/x/affiliates/types/constants.go index 788eb5c9e00..71cc8a4bac9 100644 --- a/protocol/x/affiliates/types/constants.go +++ b/protocol/x/affiliates/types/constants.go @@ -23,6 +23,14 @@ var ( ReqStakedWholeCoins: 5_000, // 5000 whole coins TakerFeeSharePpm: 150_000, // 15% }, +<<<<<<< HEAD +======= + { + ReqReferredVolumeQuoteQuantums: 50_000_000_000_000, // 50 million USDC + ReqStakedWholeCoins: 100_000_000, // 100m whole coins + TakerFeeSharePpm: 250_000, // 25% + }, +>>>>>>> c29eea29 (Dont attribute new revenue if user exceeds 30d max volume and deprecate AffiliateWhitelist (#3109)) }, } diff --git a/protocol/x/affiliates/types/errors.go b/protocol/x/affiliates/types/errors.go index a60b5229305..a47df4e9d04 100644 --- a/protocol/x/affiliates/types/errors.go +++ b/protocol/x/affiliates/types/errors.go @@ -16,5 +16,9 @@ var ( ModuleName, 8, "Duplicate affiliate address for whitelist") ErrAffiliateTiersNotSet = errorsmod.Register(ModuleName, 9, "Affiliate tiers not set (affiliate program is not active)") - ErrSelfReferral = errorsmod.Register(ModuleName, 10, "Self referral not allowed") + ErrSelfReferral = errorsmod.Register(ModuleName, 10, "Self referral not allowed") + ErrUpdatingAffiliateReferredCommission = errorsmod.Register( + ModuleName, 11, "Error updating affiliate referred commission") + ErrUpdatingAttributedVolume = errorsmod.Register( + ModuleName, 12, "Error updating attributed volume") ) diff --git a/protocol/x/affiliates/types/expected_keepers.go b/protocol/x/affiliates/types/expected_keepers.go index b92c4d304a8..871bbc3a599 100644 --- a/protocol/x/affiliates/types/expected_keepers.go +++ b/protocol/x/affiliates/types/expected_keepers.go @@ -10,6 +10,7 @@ import ( type StatsKeeper interface { GetStakedAmount(ctx sdk.Context, delegatorAddr string) *big.Int GetBlockStats(ctx sdk.Context) *stattypes.BlockStats + GetUserStats(ctx sdk.Context, address string) *stattypes.UserStats } type FeetiersKeeper interface { diff --git a/protocol/x/affiliates/types/keys.go b/protocol/x/affiliates/types/keys.go index 0965b44e1dd..15f2923b91a 100644 --- a/protocol/x/affiliates/types/keys.go +++ b/protocol/x/affiliates/types/keys.go @@ -13,7 +13,9 @@ const ( const ( ReferredByKeyPrefix = "RB:" - ReferredVolumeKeyPrefix = "RV:" + ReferredVolumeInWindowKeyPrefix = "RVW:" + + ReferredCommissionKeyPrefix = "RC:" AffiliateTiersKey = "AT" diff --git a/protocol/x/clob/keeper/process_operations.go b/protocol/x/clob/keeper/process_operations.go index 5e13c360deb..40746de6468 100644 --- a/protocol/x/clob/keeper/process_operations.go +++ b/protocol/x/clob/keeper/process_operations.go @@ -15,6 +15,7 @@ import ( "github.com/dydxprotocol/v4-chain/protocol/lib" "github.com/dydxprotocol/v4-chain/protocol/lib/log" "github.com/dydxprotocol/v4-chain/protocol/lib/metrics" + affiliatetypes "github.com/dydxprotocol/v4-chain/protocol/x/affiliates/types" "github.com/dydxprotocol/v4-chain/protocol/x/clob/types" satypes "github.com/dydxprotocol/v4-chain/protocol/x/subaccounts/types" ) @@ -136,7 +137,8 @@ func (k Keeper) ProcessInternalOperations( // All short term orders in this map have passed validation. placedShortTermOrders := make(map[types.OrderId]types.Order, 0) - var affiliatesWhitelistMap map[string]uint32 = nil + var affiliateOverrides map[string]bool = nil + var affiliateParameters affiliatetypes.AffiliateParameters // Write the matches to state if all stateful validation passes. for _, operation := range operations { if err := k.validateInternalOperationAgainstClobPairStatus(ctx, operation); err != nil { @@ -148,9 +150,9 @@ func (k Keeper) ProcessInternalOperations( // check if affiliate whitelist map is nil and initialize it if it is. // This is done to avoid getting whitelist map on list of operations // where there are no matches. - if affiliatesWhitelistMap == nil { + if affiliateOverrides == nil { var err error - affiliatesWhitelistMap, err = k.affiliatesKeeper.GetAffiliateWhitelistMap(ctx) + affiliateOverrides, err = k.affiliatesKeeper.GetAffiliateOverridesMap(ctx) if err != nil { return errorsmod.Wrapf( err, @@ -158,8 +160,17 @@ func (k Keeper) ProcessInternalOperations( ) } } + var err error + affiliateParameters, err = k.affiliatesKeeper.GetAffiliateParameters(ctx) + if err != nil { + return errorsmod.Wrapf( + err, + "ProcessInternalOperations: Failed to get affiliates parameters", + ) + } clobMatch := castedOperation.Match - if err := k.PersistMatchToState(ctx, clobMatch, placedShortTermOrders, affiliatesWhitelistMap); err != nil { + if err := k.PersistMatchToState(ctx, clobMatch, placedShortTermOrders, + affiliateOverrides, affiliateParameters); err != nil { return errorsmod.Wrapf( err, "ProcessInternalOperations: Failed to process clobMatch: %+v", @@ -215,11 +226,13 @@ func (k Keeper) PersistMatchToState( ctx sdk.Context, clobMatch *types.ClobMatch, ordersMap map[types.OrderId]types.Order, - affiliatesWhitelistMap map[string]uint32, + affiliateOverrides map[string]bool, + affiliateParameters affiliatetypes.AffiliateParameters, ) error { switch castedMatch := clobMatch.Match.(type) { case *types.ClobMatch_MatchOrders: - if err := k.PersistMatchOrdersToState(ctx, castedMatch.MatchOrders, ordersMap, affiliatesWhitelistMap); err != nil { + if err := k.PersistMatchOrdersToState(ctx, castedMatch.MatchOrders, ordersMap, + affiliateOverrides, affiliateParameters); err != nil { return err } case *types.ClobMatch_MatchPerpetualLiquidation: @@ -227,7 +240,8 @@ func (k Keeper) PersistMatchToState( ctx, castedMatch.MatchPerpetualLiquidation, ordersMap, - affiliatesWhitelistMap, + affiliateOverrides, + affiliateParameters, ); err != nil { return err } @@ -459,7 +473,8 @@ func (k Keeper) PersistMatchOrdersToState( ctx sdk.Context, matchOrders *types.MatchOrders, ordersMap map[types.OrderId]types.Order, - affiliatesWhitelistMap map[string]uint32, + affiliateOverrides map[string]bool, + affiliateParameters affiliatetypes.AffiliateParameters, ) error { takerOrderId := matchOrders.GetTakerOrderId() // Fetch the taker order from either short term orders or state @@ -504,7 +519,12 @@ func (k Keeper) PersistMatchOrdersToState( } makerOrders = append(makerOrders, makerOrder) - _, _, _, affiliateRevSharesQuoteQuantums, err := k.ProcessSingleMatch(ctx, &matchWithOrders, affiliatesWhitelistMap) + _, _, _, affiliateRevSharesQuoteQuantums, err := k.ProcessSingleMatch( + ctx, + &matchWithOrders, + affiliateOverrides, + affiliateParameters, + ) if err != nil { return err } @@ -582,7 +602,8 @@ func (k Keeper) PersistMatchLiquidationToState( ctx sdk.Context, matchLiquidation *types.MatchPerpetualLiquidation, ordersMap map[types.OrderId]types.Order, - affiliatesWhitelistMap map[string]uint32, + affiliateOverrides map[string]bool, + affiliateParameters affiliatetypes.AffiliateParameters, ) error { // If the subaccount is not liquidatable, do nothing. if err := k.EnsureIsLiquidatable(ctx, matchLiquidation.Liquidated); err != nil { @@ -623,7 +644,8 @@ func (k Keeper) PersistMatchLiquidationToState( _, _, _, affiliateRevSharesQuoteQuantums, err := k.ProcessSingleMatch( ctx, &matchWithOrders, - affiliatesWhitelistMap, + affiliateOverrides, + affiliateParameters, ) if err != nil { return err diff --git a/protocol/x/clob/keeper/process_single_match.go b/protocol/x/clob/keeper/process_single_match.go index 3c43a440472..22fe1ab6df7 100644 --- a/protocol/x/clob/keeper/process_single_match.go +++ b/protocol/x/clob/keeper/process_single_match.go @@ -12,6 +12,7 @@ import ( "github.com/dydxprotocol/v4-chain/protocol/lib" "github.com/dydxprotocol/v4-chain/protocol/lib/log" "github.com/dydxprotocol/v4-chain/protocol/lib/metrics" + affiliatetypes "github.com/dydxprotocol/v4-chain/protocol/x/affiliates/types" 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" @@ -42,7 +43,8 @@ import ( func (k Keeper) ProcessSingleMatch( ctx sdk.Context, matchWithOrders *types.MatchWithOrders, - affiliatesWhitelistMap map[string]uint32, + affiliateOverrides map[string]bool, + affiliateParameters affiliatetypes.AffiliateParameters, ) ( success bool, takerUpdateResult satypes.UpdateResult, @@ -226,7 +228,8 @@ func (k Keeper) ProcessSingleMatch( makerFeePpm, bigFillQuoteQuantums, takerInsuranceFundDelta, - affiliatesWhitelistMap, + affiliateOverrides, + affiliateParameters, ) if err != nil { @@ -332,7 +335,8 @@ func (k Keeper) persistMatchedOrders( makerFeePpm int32, bigFillQuoteQuantums *big.Int, insuranceFundDelta *big.Int, - affiliatesWhitelistMap map[string]uint32, + affiliateOverrides map[string]bool, + affiliateParameters affiliatetypes.AffiliateParameters, ) ( takerUpdateResult satypes.UpdateResult, makerUpdateResult satypes.UpdateResult, @@ -538,7 +542,12 @@ func (k Keeper) persistMatchedOrders( // Distribute the fee amount from subacounts module to fee collector and rev share accounts bigTotalFeeQuoteQuantums := new(big.Int).Add(bigTakerFeeQuoteQuantums, bigMakerFeeQuoteQuantums) - revSharesForFill, err := k.revshareKeeper.GetAllRevShares(ctx, fillForProcess, affiliatesWhitelistMap) + revSharesForFill, err := k.revshareKeeper.GetAllRevShares( + ctx, + fillForProcess, + affiliateOverrides, + affiliateParameters, + ) if err != nil { revSharesForFill = revsharetypes.RevSharesForFill{} log.ErrorLogWithError(ctx, "error getting rev shares for fill", err) @@ -578,6 +587,7 @@ func (k Keeper) persistMatchedOrders( matchWithOrders.TakerOrder.GetSubaccountId().Owner, matchWithOrders.MakerOrder.GetSubaccountId().Owner, bigFillQuoteQuantums, + affiliateRevSharesQuoteQuantums, ) takerOrderRouterFeeQuoteQuantums := big.NewInt(0) diff --git a/protocol/x/clob/memclob/memclob.go b/protocol/x/clob/memclob/memclob.go index 89c077cf9a5..cd7cbb607f5 100644 --- a/protocol/x/clob/memclob/memclob.go +++ b/protocol/x/clob/memclob/memclob.go @@ -21,6 +21,7 @@ import ( "github.com/dydxprotocol/v4-chain/protocol/lib" "github.com/dydxprotocol/v4-chain/protocol/lib/log" "github.com/dydxprotocol/v4-chain/protocol/lib/metrics" + affiliatetypes "github.com/dydxprotocol/v4-chain/protocol/x/affiliates/types" "github.com/dydxprotocol/v4-chain/protocol/x/clob/types" perptypes "github.com/dydxprotocol/v4-chain/protocol/x/perpetuals/types" satypes "github.com/dydxprotocol/v4-chain/protocol/x/subaccounts/types" @@ -1747,7 +1748,7 @@ func (m *MemClobPriceTimePriority) mustPerformTakerOrderMatching( // shares/fees are distributed to the recipient’s bank balance and not settled at the subaccount level, // and won’t affect the collateralization of future operations in the operations queue. success, takerUpdateResult, makerUpdateResult, _, err := m.clobKeeper.ProcessSingleMatch( - ctx, &matchWithOrders, map[string]uint32{}) + ctx, &matchWithOrders, map[string]bool{}, affiliatetypes.AffiliateParameters{}) if err != nil && !errors.Is(err, satypes.ErrFailedToUpdateSubaccounts) { if errors.Is(err, types.ErrLiquidationExceedsSubaccountMaxInsuranceLost) { // Subaccount has reached max insurance lost block limit. Stop matching. diff --git a/protocol/x/clob/types/clob_keeper.go b/protocol/x/clob/types/clob_keeper.go index f9ab736012e..ad29d1f4eaa 100644 --- a/protocol/x/clob/types/clob_keeper.go +++ b/protocol/x/clob/types/clob_keeper.go @@ -6,6 +6,7 @@ import ( "cosmossdk.io/log" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/dydxprotocol/v4-chain/protocol/indexer/indexer_manager" + "github.com/dydxprotocol/v4-chain/protocol/x/affiliates/types" satypes "github.com/dydxprotocol/v4-chain/protocol/x/subaccounts/types" ) @@ -81,7 +82,8 @@ type ClobKeeper interface { ProcessSingleMatch( ctx sdk.Context, matchWithOrders *MatchWithOrders, - affiliatesWhitelistMap map[string]uint32, + affiliateOverrides map[string]bool, + affiliateParameters types.AffiliateParameters, ) ( success bool, takerUpdateResult satypes.UpdateResult, diff --git a/protocol/x/clob/types/expected_keepers.go b/protocol/x/clob/types/expected_keepers.go index 5b0b0f13b5d..db61a50e89f 100644 --- a/protocol/x/clob/types/expected_keepers.go +++ b/protocol/x/clob/types/expected_keepers.go @@ -7,6 +7,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/dydxprotocol/v4-chain/protocol/lib/margin" + affiliatetypes "github.com/dydxprotocol/v4-chain/protocol/x/affiliates/types" assettypes "github.com/dydxprotocol/v4-chain/protocol/x/assets/types" blocktimetypes "github.com/dydxprotocol/v4-chain/protocol/x/blocktime/types" perpetualsmoduletypes "github.com/dydxprotocol/v4-chain/protocol/x/perpetuals/types" @@ -162,7 +163,8 @@ type PricesKeeper interface { } type StatsKeeper interface { - RecordFill(ctx sdk.Context, takerAddress string, makerAddress string, notional *big.Int) + RecordFill(ctx sdk.Context, takerAddress string, makerAddress string, + notional *big.Int, affiliateFeeGenerated *big.Int) GetUserStats(ctx sdk.Context, address string) *stattypes.UserStats } @@ -189,7 +191,8 @@ type RevShareKeeper interface { GetAllRevShares( ctx sdk.Context, fill FillForProcess, - affiliateWhitelistMap map[string]uint32, + affiliateOverrides map[string]bool, + affiliateParameters affiliatetypes.AffiliateParameters, ) ( revsharetypes.RevSharesForFill, error, ) @@ -199,6 +202,8 @@ type RevShareKeeper interface { 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) } type AccountPlusKeeper interface { diff --git a/protocol/x/clob/types/mem_clob_keeper.go b/protocol/x/clob/types/mem_clob_keeper.go index bd396a6c169..f1aa917e832 100644 --- a/protocol/x/clob/types/mem_clob_keeper.go +++ b/protocol/x/clob/types/mem_clob_keeper.go @@ -6,6 +6,7 @@ import ( "cosmossdk.io/log" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/dydxprotocol/v4-chain/protocol/indexer/indexer_manager" + "github.com/dydxprotocol/v4-chain/protocol/x/affiliates/types" satypes "github.com/dydxprotocol/v4-chain/protocol/x/subaccounts/types" ) @@ -21,7 +22,8 @@ type MemClobKeeper interface { ProcessSingleMatch( ctx sdk.Context, matchWithOrders *MatchWithOrders, - affiliatesWhitelistMap map[string]uint32, + affiliateOverrides map[string]bool, + affiliateParameters types.AffiliateParameters, ) ( success bool, takerUpdateResult satypes.UpdateResult, diff --git a/protocol/x/revshare/keeper/revshare.go b/protocol/x/revshare/keeper/revshare.go index 483935a5f4d..dd973e20f2e 100644 --- a/protocol/x/revshare/keeper/revshare.go +++ b/protocol/x/revshare/keeper/revshare.go @@ -192,7 +192,8 @@ func (k Keeper) ValidateRevShareSafety( func (k Keeper) GetAllRevShares( ctx sdk.Context, fill clobtypes.FillForProcess, - affiliatesWhitelistMap map[string]uint32, + affiliateOverrides map[string]bool, + affiliateParameters affiliatetypes.AffiliateParameters, ) (types.RevSharesForFill, error) { revShares := []types.RevShare{} feeSourceToQuoteQuantums := make(map[types.RevShareFeeSource]*big.Int) @@ -214,7 +215,8 @@ func (k Keeper) GetAllRevShares( return types.RevSharesForFill{}, nil } - affiliateRevShares, affiliateFeesShared, err := k.getAffiliateRevShares(ctx, fill, affiliatesWhitelistMap) + affiliateRevShares, affiliateFeesShared, err := k.getAffiliateRevShares( + ctx, fill, affiliateOverrides, affiliateParameters) if err != nil { return types.RevSharesForFill{}, err } @@ -291,7 +293,8 @@ func (k Keeper) GetAllRevShares( func (k Keeper) getAffiliateRevShares( ctx sdk.Context, fill clobtypes.FillForProcess, - affiliatesWhitelistMap map[string]uint32, + affiliateOverrides map[string]bool, + _ affiliatetypes.AffiliateParameters, ) ([]types.RevShare, *big.Int, error) { takerAddr := fill.TakerAddr takerFee := fill.TakerFeeQuoteQuantums @@ -301,7 +304,7 @@ func (k Keeper) getAffiliateRevShares( } takerAffiliateAddr, feeSharePpm, exists, err := k.affiliatesKeeper.GetTakerFeeShare( - ctx, takerAddr, affiliatesWhitelistMap) + ctx, takerAddr, affiliateOverrides) if err != nil { return nil, big.NewInt(0), err } diff --git a/protocol/x/revshare/keeper/revshare_test.go b/protocol/x/revshare/keeper/revshare_test.go index 0ee8ff85d31..2fd3f45b6d1 100644 --- a/protocol/x/revshare/keeper/revshare_test.go +++ b/protocol/x/revshare/keeper/revshare_test.go @@ -939,13 +939,8 @@ func TestKeeper_GetAllRevShares_Valid(t *testing.T) { err = keeper.SetOrderRouterRevShare(ctx, constants.DaveAccAddress.String(), 200_000) // 20% require.NoError(t, err) - err = affiliatesKeeper.SetAffiliateWhitelist(ctx, affiliatetypes.AffiliateWhitelist{ - Tiers: []affiliatetypes.AffiliateWhitelist_Tier{ - { - Addresses: []string{constants.BobAccAddress.String()}, - TakerFeeSharePpm: 250_000, // 25% - }, - }, + err = affiliatesKeeper.SetAffiliateOverrides(ctx, affiliatetypes.AffiliateOverrides{ + Addresses: []string{constants.BobAccAddress.String()}, }) require.NoError(t, err) }, @@ -1018,18 +1013,79 @@ func TestKeeper_GetAllRevShares_Valid(t *testing.T) { err = keeper.SetOrderRouterRevShare(ctx, constants.DaveAccAddress.String(), 200_000) // 20% require.NoError(t, err) - err = affiliatesKeeper.SetAffiliateWhitelist(ctx, affiliatetypes.AffiliateWhitelist{ - Tiers: []affiliatetypes.AffiliateWhitelist_Tier{ - { - Addresses: []string{constants.BobAccAddress.String()}, - TakerFeeSharePpm: 250_000, // 25% - }, + err = affiliatesKeeper.SetAffiliateOverrides(ctx, affiliatetypes.AffiliateOverrides{ + Addresses: []string{constants.BobAccAddress.String()}, + }) + require.NoError(t, err) + }, + }, + { +<<<<<<< HEAD +======= + name: "Rev share populates order router rev share even if one is missing", + expectedRevSharesForFill: types.RevSharesForFill{ + AllRevShares: []types.RevShare{ + { + Recipient: constants.DaveAccAddress.String(), + RevShareFeeSource: types.REV_SHARE_FEE_SOURCE_MAKER_FEE, + RevShareType: types.REV_SHARE_TYPE_ORDER_ROUTER, + QuoteQuantums: big.NewInt(400_000), + RevSharePpm: 200_000, // 20% + }, + { + Recipient: constants.AliceAccAddress.String(), + RevShareFeeSource: types.REV_SHARE_FEE_SOURCE_NET_PROTOCOL_REVENUE, + RevShareType: types.REV_SHARE_TYPE_MARKET_MAPPER, + QuoteQuantums: big.NewInt(1_160_000), + RevSharePpm: 100_000, // 10% }, + }, + AffiliateRevShare: nil, + FeeSourceToQuoteQuantums: map[types.RevShareFeeSource]*big.Int{ + types.REV_SHARE_FEE_SOURCE_NET_PROTOCOL_REVENUE: big.NewInt(1_160_000), + types.REV_SHARE_FEE_SOURCE_TAKER_FEE: big.NewInt(0), + types.REV_SHARE_FEE_SOURCE_MAKER_FEE: big.NewInt(400_000), + }, + FeeSourceToRevSharePpm: map[types.RevShareFeeSource]uint32{ + types.REV_SHARE_FEE_SOURCE_NET_PROTOCOL_REVENUE: 100_000, // 10% + types.REV_SHARE_FEE_SOURCE_TAKER_FEE: 0, // 10% + types.REV_SHARE_FEE_SOURCE_MAKER_FEE: 200_000, // 20% + }, + }, + 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: marketId, + MonthlyRollingTakerVolumeQuantums: 1_000_000_000_000, + TakerOrderRouterAddr: constants.CarlAccAddress.String(), + MakerOrderRouterAddr: constants.DaveAccAddress.String(), + }, + setup: func(tApp *testapp.TestApp, ctx sdk.Context, keeper *keeper.Keeper, + affiliatesKeeper *affiliateskeeper.Keeper) { + err := keeper.SetMarketMapperRevenueShareParams(ctx, types.MarketMapperRevenueShareParams{ + Address: constants.AliceAccAddress.String(), + RevenueSharePpm: 100_000, // 10% + ValidDays: 1, + }) + require.NoError(t, err) + + err = affiliatesKeeper.UpdateAffiliateTiers(ctx, affiliatetypes.DefaultAffiliateTiers) + require.NoError(t, err) + + err = keeper.SetOrderRouterRevShare(ctx, constants.DaveAccAddress.String(), 200_000) // 20% + require.NoError(t, err) + + err = affiliatesKeeper.SetAffiliateOverrides(ctx, affiliatetypes.AffiliateOverrides{ + Addresses: []string{constants.BobAccAddress.String()}, }) require.NoError(t, err) }, }, { +>>>>>>> c29eea29 (Dont attribute new revenue if user exceeds 30d max volume and deprecate AffiliateWhitelist (#3109)) name: "Rev share populates order router rev share if affiliates is empty", expectedRevSharesForFill: types.RevSharesForFill{ AllRevShares: []types.RevShare{ @@ -1095,13 +1151,8 @@ func TestKeeper_GetAllRevShares_Valid(t *testing.T) { err = keeper.SetOrderRouterRevShare(ctx, constants.DaveAccAddress.String(), 200_000) // 20% require.NoError(t, err) - err = affiliatesKeeper.SetAffiliateWhitelist(ctx, affiliatetypes.AffiliateWhitelist{ - Tiers: []affiliatetypes.AffiliateWhitelist_Tier{ - { - Addresses: []string{constants.BobAccAddress.String()}, - TakerFeeSharePpm: 250_000, // 25% - }, - }, + err = affiliatesKeeper.SetAffiliateOverrides(ctx, affiliatetypes.AffiliateOverrides{ + Addresses: []string{constants.BobAccAddress.String()}, }) require.NoError(t, err) }, @@ -1270,10 +1321,12 @@ func TestKeeper_GetAllRevShares_Valid(t *testing.T) { } keeper.CreateNewMarketRevShare(ctx, marketId) - affiliateWhitelistMap, err := affiliatesKeeper.GetAffiliateWhitelistMap(ctx) + affiliateOverridesMap, err := affiliatesKeeper.GetAffiliateOverridesMap(ctx) + require.NoError(t, err) + affiliateParameters, err := affiliatesKeeper.GetAffiliateParameters(ctx) require.NoError(t, err) - revSharesForFill, err := keeper.GetAllRevShares(ctx, tc.fill, affiliateWhitelistMap) + revSharesForFill, err := keeper.GetAllRevShares(ctx, tc.fill, affiliateOverridesMap, affiliateParameters) require.NoError(t, err) require.Equal(t, tc.expectedRevSharesForFill, revSharesForFill) @@ -1373,7 +1426,7 @@ func TestKeeper_GetAllRevShares_Invalid(t *testing.T) { keeper.CreateNewMarketRevShare(ctx, marketId) - _, err := keeper.GetAllRevShares(ctx, fill, map[string]uint32{}) + _, err := keeper.GetAllRevShares(ctx, fill, map[string]bool{}, affiliatetypes.AffiliateParameters{}) require.ErrorIs(t, err, tc.expectedError) }) diff --git a/protocol/x/stats/keeper/keeper.go b/protocol/x/stats/keeper/keeper.go index 4dc2bc40338..486d3ebb3a4 100644 --- a/protocol/x/stats/keeper/keeper.go +++ b/protocol/x/stats/keeper/keeper.go @@ -27,6 +27,7 @@ type ( transientStoreKey storetypes.StoreKey authorities map[string]struct{} stakingKeeper types.StakingKeeper + expirationHooks []types.StatsExpirationHook } ) @@ -79,14 +80,21 @@ func (k Keeper) SetBlockStats(ctx sdk.Context, blockStats *types.BlockStats) { } // Record a match in BlockStats, which is stored in the transient store -func (k Keeper) RecordFill(ctx sdk.Context, takerAddress string, makerAddress string, notional *big.Int) { +func (k Keeper) RecordFill( + ctx sdk.Context, + takerAddress string, + makerAddress string, + notional *big.Int, + affiliateFeeGenerated *big.Int, +) { blockStats := k.GetBlockStats(ctx) blockStats.Fills = append( blockStats.Fills, &types.BlockStats_Fill{ - Taker: takerAddress, - Maker: makerAddress, - Notional: notional.Uint64(), + Taker: takerAddress, + Maker: makerAddress, + Notional: notional.Uint64(), + AffiliateFeeGeneratedQuantums: affiliateFeeGenerated.Uint64(), }, ) k.SetBlockStats(ctx, blockStats) @@ -202,6 +210,8 @@ func (k Keeper) ProcessBlockStats(ctx sdk.Context) { for _, fill := range blockStats.Fills { userStats := k.GetUserStats(ctx, fill.Taker) userStats.TakerNotional += fill.Notional + // Add affiliate revenue generated on taker for this fill (if any) + userStats.AffiliateRevenueGeneratedQuantums += fill.AffiliateFeeGeneratedQuantums k.SetUserStats(ctx, fill.Taker, userStats) userStats = k.GetUserStats(ctx, fill.Maker) @@ -222,6 +232,8 @@ func (k Keeper) ProcessBlockStats(ctx sdk.Context) { } userStatsMap[fill.Taker].Stats.TakerNotional += fill.Notional userStatsMap[fill.Maker].Stats.MakerNotional += fill.Notional + // Track affiliate revenue generated on the taker in this epoch snapshot + userStatsMap[fill.Taker].Stats.AffiliateRevenueGeneratedQuantums += fill.AffiliateFeeGeneratedQuantums globalStats := k.GetGlobalStats(ctx) globalStats.NotionalTraded += fill.Notional @@ -271,8 +283,17 @@ func (k Keeper) ExpireOldStats(ctx sdk.Context) { stats := k.GetUserStats(ctx, removedStats.User) stats.TakerNotional -= removedStats.Stats.TakerNotional stats.MakerNotional -= removedStats.Stats.MakerNotional + stats.AffiliateRevenueGeneratedQuantums -= removedStats.Stats.AffiliateRevenueGeneratedQuantums k.SetUserStats(ctx, removedStats.User, stats) + // Execute work in other keepers + for _, hook := range k.expirationHooks { + err := hook.OnStatsExpired(ctx, removedStats.User, removedStats.Stats) + if err != nil { + k.Logger(ctx).Error("failed to expire stats", "user", removedStats.User, "error", err) + } + } + // Just remove TakerNotional to avoid double counting globalStats.NotionalTraded -= removedStats.Stats.TakerNotional } @@ -282,6 +303,11 @@ func (k Keeper) ExpireOldStats(ctx sdk.Context) { k.SetStatsMetadata(ctx, metadata) } +// AddStatsExpirationHook adds a hook to be called when stats expire +func (k *Keeper) AddStatsExpirationHook(hook types.StatsExpirationHook) { + k.expirationHooks = append(k.expirationHooks, hook) +} + // GetStakedAmount returns the total staked amount for a delegator address. // It maintains a cache to optimize performance. The function first checks // if there's a cached value that hasn't expired. If found, it returns the diff --git a/protocol/x/stats/keeper/keeper_test.go b/protocol/x/stats/keeper/keeper_test.go index 682520d52da..e0690a49e15 100644 --- a/protocol/x/stats/keeper/keeper_test.go +++ b/protocol/x/stats/keeper/keeper_test.go @@ -26,9 +26,10 @@ func TestLogger(t *testing.T) { } type recordFillArgs struct { - taker string - maker string - notional *big.Int + taker string + maker string + notional *big.Int + affiliateFee *big.Int } func TestRecordFill(t *testing.T) { @@ -44,7 +45,7 @@ func TestRecordFill(t *testing.T) { }, "single fill": { []recordFillArgs{ - {"taker", "maker", new(big.Int).SetUint64(123)}, + {"taker", "maker", new(big.Int).SetUint64(123), big.NewInt(0)}, }, &types.BlockStats{ Fills: []*types.BlockStats_Fill{ @@ -58,8 +59,8 @@ func TestRecordFill(t *testing.T) { }, "multiple fills": { []recordFillArgs{ - {"alice", "bob", new(big.Int).SetUint64(123)}, - {"bob", "alice", new(big.Int).SetUint64(321)}, + {"alice", "bob", new(big.Int).SetUint64(123), big.NewInt(0)}, + {"bob", "alice", new(big.Int).SetUint64(321), big.NewInt(0)}, }, &types.BlockStats{ Fills: []*types.BlockStats_Fill{ @@ -85,7 +86,7 @@ func TestRecordFill(t *testing.T) { k := tApp.App.StatsKeeper for _, fill := range tc.args { - k.RecordFill(ctx, fill.taker, fill.maker, fill.notional) + k.RecordFill(ctx, fill.taker, fill.maker, fill.notional, fill.affiliateFee) } require.Equal(t, tc.expectedBlockStats, k.GetBlockStats(ctx)) }) diff --git a/protocol/x/stats/types/expected_keepers.go b/protocol/x/stats/types/expected_keepers.go index a33ee70d273..bdc70d6051c 100644 --- a/protocol/x/stats/types/expected_keepers.go +++ b/protocol/x/stats/types/expected_keepers.go @@ -17,3 +17,8 @@ type StakingKeeper interface { GetDelegatorDelegations(ctx context.Context, delegator sdk.AccAddress, maxRetrieve uint16) ([]stakingtypes.Delegation, error) } + +// StatsExpirationHook is called when stats are expired from the rolling window +type StatsExpirationHook interface { + OnStatsExpired(ctx sdk.Context, userAddress string, resultingUserStats *UserStats) error +} diff --git a/protocol/x/stats/types/stats.pb.go b/protocol/x/stats/types/stats.pb.go index 7c0a6f59a9a..95c0c0ce760 100644 --- a/protocol/x/stats/types/stats.pb.go +++ b/protocol/x/stats/types/stats.pb.go @@ -81,7 +81,12 @@ type BlockStats_Fill struct { // Maker wallet address Maker string `protobuf:"bytes,2,opt,name=maker,proto3" json:"maker,omitempty"` // Notional USDC filled in quantums + // Used to calculate fee tier, and affiliate revenue attributed for taker Notional uint64 `protobuf:"varint,3,opt,name=notional,proto3" json:"notional,omitempty"` + // Affiliate fee generated in quantums of the taker fee for the affiliate + // 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"` } func (m *BlockStats_Fill) Reset() { *m = BlockStats_Fill{} } @@ -138,6 +143,13 @@ func (m *BlockStats_Fill) GetNotional() uint64 { return 0 } +func (m *BlockStats_Fill) GetAffiliateFeeGeneratedQuantums() uint64 { + if m != nil { + return m.AffiliateFeeGeneratedQuantums + } + return 0 +} + // 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 @@ -293,7 +305,7 @@ func (m *EpochStats_UserWithStats) GetStats() *UserStats { return nil } -// GlobalStats stores global stats +// GlobalStats stores global stats for the rolling window (default 30d). type GlobalStats struct { // Notional USDC traded in quantums NotionalTraded uint64 `protobuf:"varint,1,opt,name=notional_traded,json=notionalTraded,proto3" json:"notional_traded,omitempty"` @@ -339,12 +351,15 @@ func (m *GlobalStats) GetNotionalTraded() uint64 { return 0 } -// UserStats stores stats for a User +// UserStats stores stats for a User. This is the sum of all stats for a user in +// the rolling window (default 30d). type UserStats struct { // Taker USDC in quantums TakerNotional uint64 `protobuf:"varint,1,opt,name=taker_notional,json=takerNotional,proto3" json:"taker_notional,omitempty"` // Maker USDC in quantums MakerNotional uint64 `protobuf:"varint,2,opt,name=maker_notional,json=makerNotional,proto3" json:"maker_notional,omitempty"` + // Affiliate revenue generated in quantums + AffiliateRevenueGeneratedQuantums uint64 `protobuf:"varint,3,opt,name=affiliate_revenue_generated_quantums,json=affiliateRevenueGeneratedQuantums,proto3" json:"affiliate_revenue_generated_quantums,omitempty"` } func (m *UserStats) Reset() { *m = UserStats{} } @@ -394,6 +409,13 @@ func (m *UserStats) GetMakerNotional() uint64 { return 0 } +func (m *UserStats) GetAffiliateRevenueGeneratedQuantums() uint64 { + if m != nil { + return m.AffiliateRevenueGeneratedQuantums + } + return 0 +} + // CachedStakeAmount stores the last calculated total staked amount for address type CachedStakeAmount struct { // Last calculated total staked amount by the delegator (in coin amount). @@ -457,41 +479,46 @@ func init() { func init() { proto.RegisterFile("dydxprotocol/stats/stats.proto", fileDescriptor_07475747e6dcccdc) } var fileDescriptor_07475747e6dcccdc = []byte{ - // 542 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x93, 0xcb, 0x6e, 0xd3, 0x4e, - 0x14, 0xc6, 0xe3, 0x5c, 0xfe, 0x4a, 0x26, 0x71, 0xfe, 0x62, 0xd4, 0x45, 0x64, 0x84, 0x13, 0x19, - 0x21, 0xb2, 0x80, 0xb1, 0xd4, 0xa2, 0x22, 0x76, 0x34, 0xa8, 0xdc, 0x24, 0x2a, 0xe1, 0x14, 0x71, - 0xd9, 0x58, 0x13, 0x7b, 0xea, 0x8c, 0x3a, 0xf6, 0x44, 0xf6, 0x04, 0xb5, 0x3c, 0x45, 0x77, 0x2c, - 0x79, 0x9d, 0x2e, 0xbb, 0x44, 0x2c, 0x0a, 0x4a, 0xde, 0x81, 0x35, 0x9a, 0x33, 0xb9, 0x8a, 0x2e, - 0xd8, 0x58, 0x73, 0x7e, 0xf3, 0x9d, 0xf3, 0x1d, 0x7f, 0x96, 0x91, 0x1b, 0x9f, 0xc7, 0x67, 0x93, - 0x5c, 0x2a, 0x19, 0x49, 0xe1, 0x17, 0x8a, 0xaa, 0xc2, 0x3c, 0x09, 0x40, 0x8c, 0x37, 0xef, 0x09, - 0xdc, 0x38, 0x3b, 0x89, 0x4c, 0x24, 0x30, 0x5f, 0x9f, 0x8c, 0xd2, 0xe9, 0x26, 0x52, 0x26, 0x82, - 0xf9, 0x50, 0x8d, 0xa6, 0x27, 0xbe, 0xe2, 0x29, 0x2b, 0x14, 0x4d, 0x27, 0x46, 0xe0, 0x7d, 0xb5, - 0x10, 0x1a, 0x08, 0x19, 0x9d, 0x0e, 0xf5, 0x14, 0xfc, 0x04, 0xd5, 0x4e, 0xb8, 0x10, 0x45, 0xc7, - 0xea, 0x55, 0xfa, 0xcd, 0xdd, 0xbb, 0xe4, 0x6f, 0x27, 0xb2, 0x96, 0x93, 0xe7, 0x5c, 0x88, 0xc0, - 0x74, 0x38, 0x47, 0xa8, 0xaa, 0x4b, 0xbc, 0x83, 0x6a, 0x8a, 0x9e, 0xb2, 0xbc, 0x63, 0xf5, 0xac, - 0x7e, 0x23, 0x30, 0x85, 0xa6, 0x29, 0xd0, 0xb2, 0xa1, 0x50, 0x60, 0x07, 0xd5, 0x33, 0xa9, 0xb8, - 0xcc, 0xa8, 0xe8, 0x54, 0x7a, 0x56, 0xbf, 0x1a, 0xac, 0x6a, 0x6f, 0x1f, 0xd9, 0x60, 0xf2, 0x86, - 0x29, 0x1a, 0x53, 0x45, 0xf1, 0x3d, 0xd4, 0x56, 0x39, 0xe5, 0x82, 0x67, 0x49, 0xc8, 0x26, 0x32, - 0x1a, 0x83, 0x83, 0x1d, 0xd8, 0x4b, 0x7a, 0xa8, 0xa1, 0xf7, 0xdb, 0x42, 0x08, 0x4e, 0xe6, 0x8d, - 0x5e, 0xa3, 0x36, 0x88, 0x43, 0x96, 0xc5, 0xa1, 0x7e, 0x7b, 0xe8, 0x6a, 0xee, 0x3a, 0xc4, 0x44, - 0x43, 0x96, 0xd1, 0x90, 0xe3, 0x65, 0x34, 0x83, 0xfa, 0xe5, 0x75, 0xb7, 0x74, 0xf1, 0xb3, 0x6b, - 0x05, 0x2d, 0xe8, 0x3d, 0xcc, 0x62, 0x7d, 0x89, 0x07, 0xa8, 0x06, 0x11, 0x74, 0xca, 0x90, 0xce, - 0x83, 0x9b, 0xd2, 0x59, 0x5b, 0x93, 0x77, 0x05, 0xcb, 0xdf, 0x73, 0x65, 0xaa, 0xc0, 0xb4, 0x3a, - 0x1f, 0x90, 0xbd, 0xc5, 0x31, 0x46, 0xd5, 0x69, 0xb1, 0x8a, 0x0b, 0xce, 0x78, 0x6f, 0x6d, 0xa4, - 0x77, 0xbd, 0x73, 0x93, 0x91, 0x9e, 0xb2, 0x39, 0xd9, 0xdb, 0x47, 0xcd, 0x17, 0x42, 0x8e, 0xa8, - 0x30, 0x73, 0xef, 0xa3, 0xff, 0x97, 0x59, 0x86, 0x2a, 0xa7, 0x31, 0x8b, 0xc1, 0xa2, 0x1a, 0xb4, - 0x97, 0xf8, 0x18, 0xa8, 0xf7, 0x11, 0x35, 0x56, 0xb3, 0x20, 0x64, 0xfd, 0x69, 0xc2, 0xd5, 0x77, - 0x31, 0x4d, 0x36, 0xd0, 0xa3, 0x05, 0xd4, 0xb2, 0x74, 0x5b, 0x56, 0x36, 0xb2, 0x74, 0x53, 0xe6, - 0x7d, 0xb3, 0xd0, 0xad, 0x67, 0x34, 0x1a, 0xb3, 0x78, 0xa8, 0xfb, 0x0f, 0x52, 0x39, 0xcd, 0x14, - 0x4e, 0x91, 0x5d, 0xe8, 0x32, 0x0e, 0x29, 0x00, 0xb0, 0x68, 0x0d, 0x5e, 0xea, 0xd4, 0x7f, 0x5c, - 0x77, 0x9f, 0x26, 0x5c, 0x8d, 0xa7, 0x23, 0x12, 0xc9, 0xd4, 0xdf, 0xfa, 0x11, 0x3e, 0x3f, 0x7a, - 0x18, 0x8d, 0x29, 0xcf, 0xfc, 0x15, 0x89, 0xd5, 0xf9, 0x84, 0x15, 0x64, 0xc8, 0x72, 0x4e, 0x05, - 0xff, 0x42, 0x47, 0x82, 0xbd, 0xca, 0x54, 0xd0, 0x32, 0xe3, 0x17, 0x76, 0xb7, 0x51, 0x23, 0x82, - 0x1d, 0x42, 0xaa, 0x60, 0xcd, 0x4a, 0x50, 0x37, 0xe0, 0x40, 0x0d, 0xde, 0x5e, 0xce, 0x5c, 0xeb, - 0x6a, 0xe6, 0x5a, 0xbf, 0x66, 0xae, 0x75, 0x31, 0x77, 0x4b, 0x57, 0x73, 0xb7, 0xf4, 0x7d, 0xee, - 0x96, 0x3e, 0x3d, 0xfe, 0xf7, 0x35, 0xce, 0x16, 0xff, 0x28, 0x6c, 0x33, 0xfa, 0x0f, 0xf8, 0xde, - 0x9f, 0x00, 0x00, 0x00, 0xff, 0xff, 0x83, 0x7a, 0xe4, 0x41, 0xc6, 0x03, 0x00, 0x00, + // 609 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x54, 0x41, 0x4f, 0xd4, 0x40, + 0x14, 0xde, 0x81, 0xc5, 0xc0, 0x83, 0x5d, 0xe3, 0x84, 0xc3, 0x66, 0x0d, 0xdd, 0x75, 0x95, 0xc8, + 0x41, 0xbb, 0x09, 0x18, 0x8c, 0x37, 0x59, 0x03, 0xa8, 0x89, 0x1a, 0x0a, 0x46, 0xe3, 0xa5, 0x99, + 0x6d, 0x67, 0xbb, 0x13, 0xa6, 0x9d, 0xb5, 0x9d, 0x12, 0xf0, 0x57, 0x70, 0xf3, 0xe8, 0xdd, 0x5f, + 0xc2, 0x91, 0xa3, 0xf1, 0x80, 0x0a, 0xff, 0xc1, 0xb3, 0x99, 0x37, 0xb4, 0xcb, 0x06, 0x0e, 0x5e, + 0x9a, 0x79, 0xdf, 0x7c, 0xef, 0xbd, 0xaf, 0xdf, 0x7b, 0x2d, 0x38, 0xe1, 0x51, 0x78, 0x38, 0x4a, + 0x95, 0x56, 0x81, 0x92, 0xdd, 0x4c, 0x33, 0x9d, 0xd9, 0xa7, 0x8b, 0x20, 0xa5, 0x57, 0xef, 0x5d, + 0xbc, 0x69, 0x2e, 0x46, 0x2a, 0x52, 0x88, 0x75, 0xcd, 0xc9, 0x32, 0x9b, 0xad, 0x48, 0xa9, 0x48, + 0xf2, 0x2e, 0x46, 0xfd, 0x7c, 0xd0, 0xd5, 0x22, 0xe6, 0x99, 0x66, 0xf1, 0xc8, 0x12, 0x3a, 0x7f, + 0x08, 0x40, 0x4f, 0xaa, 0x60, 0x7f, 0xd7, 0x54, 0xa1, 0xcf, 0x60, 0x66, 0x20, 0xa4, 0xcc, 0x1a, + 0xa4, 0x3d, 0xbd, 0x32, 0xbf, 0x7a, 0xdf, 0xbd, 0xde, 0xc9, 0x1d, 0xd3, 0xdd, 0x2d, 0x21, 0xa5, + 0x67, 0x33, 0x9a, 0x5f, 0x09, 0x54, 0x4d, 0x4c, 0x17, 0x61, 0x46, 0xb3, 0x7d, 0x9e, 0x36, 0x48, + 0x9b, 0xac, 0xcc, 0x79, 0x36, 0x30, 0x68, 0x8c, 0xe8, 0x94, 0x45, 0x31, 0xa0, 0x4d, 0x98, 0x4d, + 0x94, 0x16, 0x2a, 0x61, 0xb2, 0x31, 0xdd, 0x26, 0x2b, 0x55, 0xaf, 0x8c, 0xe9, 0x36, 0xb4, 0xd9, + 0x60, 0x20, 0xa4, 0x60, 0x9a, 0xfb, 0x03, 0xce, 0xfd, 0x88, 0x27, 0x3c, 0x65, 0x9a, 0x87, 0xfe, + 0xe7, 0x9c, 0x25, 0x3a, 0x8f, 0xb3, 0x46, 0x15, 0x73, 0x96, 0x4a, 0xde, 0x16, 0xe7, 0xdb, 0x05, + 0x6b, 0xe7, 0x92, 0xd4, 0x59, 0x87, 0x1a, 0xca, 0x7d, 0xc3, 0x35, 0x0b, 0x99, 0x66, 0x74, 0x19, + 0xea, 0x3a, 0x65, 0x42, 0x8a, 0x24, 0xf2, 0xf9, 0x48, 0x05, 0x43, 0x94, 0x5a, 0xf3, 0x6a, 0x05, + 0xba, 0x69, 0xc0, 0xce, 0x5f, 0x02, 0x80, 0x27, 0xeb, 0xcd, 0x6b, 0xa8, 0x23, 0xd9, 0xe7, 0x49, + 0xe8, 0x1b, 0x1f, 0x31, 0x6b, 0x7e, 0xb5, 0xe9, 0x5a, 0x93, 0xdd, 0xc2, 0x64, 0x77, 0xaf, 0x30, + 0xb9, 0x37, 0x7b, 0x72, 0xd6, 0xaa, 0x1c, 0xff, 0x6a, 0x11, 0x6f, 0x01, 0x73, 0x37, 0x93, 0xd0, + 0x5c, 0xd2, 0x1e, 0xcc, 0xa0, 0x99, 0x8d, 0x29, 0xf4, 0xf9, 0xd1, 0x4d, 0x3e, 0x8f, 0x5b, 0xbb, + 0xef, 0x33, 0x9e, 0x7e, 0x10, 0xda, 0x46, 0x9e, 0x4d, 0x6d, 0x7e, 0x84, 0xda, 0x04, 0x4e, 0x29, + 0x54, 0xf3, 0xac, 0xf4, 0x1d, 0xcf, 0x74, 0x6d, 0xdc, 0xc8, 0x68, 0x5d, 0xba, 0xa9, 0x91, 0xa9, + 0x72, 0xb5, 0x72, 0x67, 0x1d, 0xe6, 0xb7, 0xa5, 0xea, 0x33, 0x69, 0xeb, 0x3e, 0x84, 0xdb, 0xc5, + 0x50, 0x7c, 0x9d, 0xb2, 0x90, 0x87, 0xd8, 0xa2, 0xea, 0xd5, 0x0b, 0x78, 0x0f, 0xd1, 0xce, 0x77, + 0x02, 0x73, 0x65, 0x31, 0x74, 0xd9, 0x0c, 0xd9, 0x2f, 0x27, 0x6c, 0xb3, 0x6a, 0x88, 0xbe, 0x2d, + 0xc6, 0xbc, 0x0c, 0xf5, 0x78, 0x92, 0x36, 0x65, 0x69, 0xf1, 0x04, 0xed, 0x1d, 0x3c, 0x18, 0x6f, + 0x43, 0xca, 0x0f, 0x78, 0x92, 0xdf, 0xb8, 0x11, 0x76, 0x8b, 0xee, 0x95, 0x5c, 0xcf, 0x52, 0xaf, + 0x6f, 0xc5, 0x37, 0x02, 0x77, 0x5e, 0xb0, 0x60, 0xc8, 0xc3, 0x5d, 0x23, 0x68, 0x23, 0x56, 0x79, + 0xa2, 0x69, 0x0c, 0xb5, 0xcc, 0x84, 0xa1, 0xcf, 0x10, 0x40, 0xcd, 0x0b, 0xbd, 0x97, 0x66, 0x8e, + 0x3f, 0xcf, 0x5a, 0xcf, 0x23, 0xa1, 0x87, 0x79, 0xdf, 0x0d, 0x54, 0xdc, 0x9d, 0xf8, 0x48, 0x0f, + 0x9e, 0x3c, 0x0e, 0x86, 0x4c, 0x24, 0xdd, 0x12, 0x09, 0xf5, 0xd1, 0x88, 0x67, 0xee, 0x2e, 0x4f, + 0x05, 0x93, 0xe2, 0x0b, 0xeb, 0x4b, 0xfe, 0x2a, 0xd1, 0xde, 0x82, 0x2d, 0x7f, 0xd9, 0xee, 0x2e, + 0xcc, 0x05, 0xa8, 0xc1, 0x67, 0x1a, 0xdf, 0x7b, 0xda, 0x9b, 0xb5, 0xc0, 0x86, 0xee, 0xed, 0x9c, + 0x9c, 0x3b, 0xe4, 0xf4, 0xdc, 0x21, 0xbf, 0xcf, 0x1d, 0x72, 0x7c, 0xe1, 0x54, 0x4e, 0x2f, 0x9c, + 0xca, 0x8f, 0x0b, 0xa7, 0xf2, 0xe9, 0xe9, 0xff, 0xcb, 0x38, 0xbc, 0xfc, 0x7f, 0xa0, 0x9a, 0xfe, + 0x2d, 0xc4, 0xd7, 0xfe, 0x05, 0x00, 0x00, 0xff, 0xff, 0x90, 0xeb, 0x88, 0xf0, 0x62, 0x04, 0x00, + 0x00, } func (m *BlockStats) Marshal() (dAtA []byte, err error) { @@ -551,6 +578,11 @@ func (m *BlockStats_Fill) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + if m.AffiliateFeeGeneratedQuantums != 0 { + i = encodeVarintStats(dAtA, i, uint64(m.AffiliateFeeGeneratedQuantums)) + i-- + dAtA[i] = 0x20 + } if m.Notional != 0 { i = encodeVarintStats(dAtA, i, uint64(m.Notional)) i-- @@ -736,6 +768,11 @@ func (m *UserStats) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + if m.AffiliateRevenueGeneratedQuantums != 0 { + i = encodeVarintStats(dAtA, i, uint64(m.AffiliateRevenueGeneratedQuantums)) + i-- + dAtA[i] = 0x18 + } if m.MakerNotional != 0 { i = encodeVarintStats(dAtA, i, uint64(m.MakerNotional)) i-- @@ -830,6 +867,9 @@ func (m *BlockStats_Fill) Size() (n int) { if m.Notional != 0 { n += 1 + sovStats(uint64(m.Notional)) } + if m.AffiliateFeeGeneratedQuantums != 0 { + n += 1 + sovStats(uint64(m.AffiliateFeeGeneratedQuantums)) + } return n } @@ -903,6 +943,9 @@ func (m *UserStats) Size() (n int) { if m.MakerNotional != 0 { n += 1 + sovStats(uint64(m.MakerNotional)) } + if m.AffiliateRevenueGeneratedQuantums != 0 { + n += 1 + sovStats(uint64(m.AffiliateRevenueGeneratedQuantums)) + } return n } @@ -1122,6 +1165,25 @@ func (m *BlockStats_Fill) Unmarshal(dAtA []byte) error { break } } + case 4: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field AffiliateFeeGeneratedQuantums", wireType) + } + m.AffiliateFeeGeneratedQuantums = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowStats + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.AffiliateFeeGeneratedQuantums |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } default: iNdEx = preIndex skippy, err := skipStats(dAtA[iNdEx:]) @@ -1583,6 +1645,25 @@ func (m *UserStats) Unmarshal(dAtA []byte) error { break } } + case 3: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field AffiliateRevenueGeneratedQuantums", wireType) + } + m.AffiliateRevenueGeneratedQuantums = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowStats + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.AffiliateRevenueGeneratedQuantums |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } default: iNdEx = preIndex skippy, err := skipStats(dAtA[iNdEx:]) From aec6086b2165d75729ea549414ea37a87af3b6b3 Mon Sep 17 00:00:00 2001 From: Justin Barnett <61020572+jusbar23@users.noreply.github.com> Date: Wed, 22 Oct 2025 16:52:24 -0400 Subject: [PATCH 2/3] Fix merge conflict in revshare_test.go --- protocol/x/revshare/keeper/revshare_test.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/protocol/x/revshare/keeper/revshare_test.go b/protocol/x/revshare/keeper/revshare_test.go index 2fd3f45b6d1..7e7506cd01b 100644 --- a/protocol/x/revshare/keeper/revshare_test.go +++ b/protocol/x/revshare/keeper/revshare_test.go @@ -1020,8 +1020,6 @@ func TestKeeper_GetAllRevShares_Valid(t *testing.T) { }, }, { -<<<<<<< HEAD -======= name: "Rev share populates order router rev share even if one is missing", expectedRevSharesForFill: types.RevSharesForFill{ AllRevShares: []types.RevShare{ @@ -1085,7 +1083,6 @@ func TestKeeper_GetAllRevShares_Valid(t *testing.T) { }, }, { ->>>>>>> c29eea29 (Dont attribute new revenue if user exceeds 30d max volume and deprecate AffiliateWhitelist (#3109)) name: "Rev share populates order router rev share if affiliates is empty", expectedRevSharesForFill: types.RevSharesForFill{ AllRevShares: []types.RevShare{ From 68153c3eef089a43eafca657594bf8836e0489fe Mon Sep 17 00:00:00 2001 From: Justin Barnett <61020572+jusbar23@users.noreply.github.com> Date: Wed, 22 Oct 2025 21:07:27 -0400 Subject: [PATCH 3/3] Implement affiliate address override for tier assignment Added logic to check for affiliate address overrides and set tier accordingly. --- protocol/x/affiliates/keeper/keeper.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/protocol/x/affiliates/keeper/keeper.go b/protocol/x/affiliates/keeper/keeper.go index 9091db97c11..dffbae5beff 100644 --- a/protocol/x/affiliates/keeper/keeper.go +++ b/protocol/x/affiliates/keeper/keeper.go @@ -263,6 +263,15 @@ func (k Keeper) GetTierForAffiliate( numTiers := uint32(len(tiers)) maxTierLevel := numTiers - 1 currentTier := uint32(0) + + // Check whether the address is overridden, if it is then set the + // affiliate tier to the max + affiliateOverrides, err := k.GetAllAffilliateOverrides(ctx) + if err != nil { + return 0, 0, err + } + for _, addr := range affiliateOverrides.Addresses { + if addr == affiliateAddr { feeSharePpm = affiliateTiers.Tiers[maxTierLevel].TakerFeeSharePpm return uint32(maxTierLevel), feeSharePpm, nil }