Skip to content

protocol fee share logic #76

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: biweekly-runs
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 69 additions & 10 deletions fee_allocator/accounting/chains.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,14 @@
GlobalFeeConfig,
RerouteConfig,
InputFees,
AllianceConfig,
AlliancePool
)
from fee_allocator.constants import (
FEE_CONSTANTS_URL,
CORE_POOLS_URL,
REROUTE_CONFIG_URL,
ALLIANCE_CONFIG_URL,
)
from fee_allocator.accounting.decorators import round
from fee_allocator.logger import logger
Expand Down Expand Up @@ -62,6 +65,7 @@ def __init__(
self.core_pools = core_pools

self.fee_config = GlobalFeeConfig(**requests.get(FEE_CONSTANTS_URL).json())
self.alliance_config = AllianceConfig(**requests.get(ALLIANCE_CONFIG_URL).json())
self.reroute_config = RerouteConfig(**requests.get(REROUTE_CONFIG_URL).json())

# caches a list of `PoolFeeData` for each chain
Expand Down Expand Up @@ -178,6 +182,7 @@ def __init__(self, chains: CorePoolRunConfig, name: str, fees: int, web3: Web3):
self.fees_collected = fees
self.web3 = web3
self.core_pools_list = self.chains.core_pools.get(self.name, {}) if self.chains.core_pools else None
self.alliance_pools: List[AlliancePool] = []

try:
self.chain_id = AddrBook.chain_ids_by_name[self.name]
Expand All @@ -191,6 +196,7 @@ def __init__(self, chains: CorePoolRunConfig, name: str, fees: int, web3: Web3):
self.block_range = self._set_block_range()
self.pool_fee_data: Union[list[PoolFeeData], None] = None
self.core_pools: List[PoolFee] = []
self.alliance_noncore_fee_data: List[PoolFee] = []

def _set_block_range(self) -> tuple[int, int]:
start = get_block_by_ts(self.chains.date_range[0], self)
Expand Down Expand Up @@ -218,12 +224,31 @@ def _cache_file_path(self) -> Path:
filename = f"{self.name}_{self.chains.protocol_version}_{self.chains.date_range[0]}_{self.chains.date_range[1]}.joblib"
return self.chains.cache_dir / filename

def _init_alliance_pools(self) -> None:
"""
Initialize alliance pools for the current chain
"""
self.alliance_pools = [
pool
for member in self.chains.alliance_config.alliance_members
for pool in member.pools
if pool.network == self.name and pool.active
]

def _load_core_pools_from_cache(self) -> list[PoolFeeData]:
logger.info(f"loading core pools from cache for {self.name}")
return joblib.load(self._cache_file_path())
cached_data = joblib.load(self._cache_file_path())
self.alliance_pools = cached_data.get('alliance_pools', [])
self.alliance_noncore_fee_data = cached_data.get('alliance_noncore_fee_data', [])
return cached_data.get('pool_fee_data', [])

def _save_core_pools_to_cache(self, pool_data: list[PoolFeeData]) -> None:
joblib.dump(pool_data, self._cache_file_path())
cache_data = {
'pool_fee_data': pool_data,
'alliance_pools': self.alliance_pools,
'alliance_noncore_fee_data': self.alliance_noncore_fee_data
}
joblib.dump(cache_data, self._cache_file_path())

def _fetch_and_process_pool_fee_data(self) -> list[PoolFeeData]:
"""
Expand All @@ -246,26 +271,46 @@ def _fetch_and_process_pool_fee_data(self) -> list[PoolFeeData]:

pools_data = []

core_pools_list = (
core_pools_list = list(
[(pool_id, label) for pool_id, label in self.core_pools_list.items()]
if self.core_pools_list is not None
else self.bal_pools_gauges.core_pools
)

self._init_alliance_pools()

alliance_pool_ids = {pool.pool_id for pool in self.alliance_pools}
alliance_pool_tuples = [(pool.pool_id, pool.partner) for pool in self.alliance_pools]

core_pools_dict = dict(core_pools_list)
core_pools_dict.update(dict(alliance_pool_tuples))
core_pools_list = list(core_pools_dict.items())

# process v3 pools
if self.chains.protocol_version == "v3":
v3_pools = [(p, l) for p, l in core_pools_list if len(p) == 42]
for pool_id, label in v3_pools:
pool_fee_data = self._fetch_twap_prices_and_init_pool_fee_data_v3(pool_id, label, pool_to_gauge)
pools_data.append(pool_fee_data)
alliance_pool = next((p for p in self.alliance_pools if p.pool_id == pool_id), None)
if alliance_pool and alliance_pool.pool_type != "core":
self.alliance_noncore_fee_data.append(pool_fee_data)
else:
pools_data.append(pool_fee_data)

# process v2 pools
elif self.chains.protocol_version == "v2":
v2_pools = [(p, l) for p, l in core_pools_list if len(p) != 42]
for pool_id, label in v2_pools:
start_snap = self._get_latest_snapshot(start_snaps, pool_id)
end_snap = self._get_latest_snapshot(end_snaps, pool_id)
if self._should_add_pool(pool_id, start_snap, end_snap, pool_to_gauge):

if pool_id in alliance_pool_ids or self._should_add_pool(pool_id, start_snap, end_snap, pool_to_gauge):
pool_fee_data = self._fetch_twap_prices_and_init_pool_fee_data_v2(pool_id, label, pool_to_gauge, start_snap, end_snap)
pools_data.append(pool_fee_data)
alliance_pool = next((p for p in self.alliance_pools if p.pool_id == pool_id), None)
if alliance_pool and alliance_pool.pool_type != "core":
self.alliance_noncore_fee_data.append(pool_fee_data)
else:
pools_data.append(pool_fee_data)

return pools_data

Expand Down Expand Up @@ -318,12 +363,12 @@ def _fetch_twap_prices_and_init_pool_fee_data_v2(
symbol=label,
bpt_price=prices.bpt_price.twap_price,
tokens_price=prices.token_prices,
gauge_address=pool_to_gauge[pool_id],
gauge_address=pool_to_gauge.get(pool_id),
start_pool_snapshot=start_snap,
end_pool_snapshot=end_snap,
last_join_exit_ts=last_join_exit_ts,
)

def _fetch_twap_prices_and_init_pool_fee_data_v3(
self,
pool_id: str,
Expand All @@ -342,12 +387,13 @@ def _fetch_twap_prices_and_init_pool_fee_data_v3(
address=pool_id,
symbol=label,
tokens_price=None,
gauge_address=pool_to_gauge[pool_id],
gauge_address=pool_to_gauge.get(pool_id),
start_pool_snapshot=None,
end_pool_snapshot=None,
last_join_exit_ts=last_join_exit_ts,
total_earned_fees_usd_twap=self.subgraph.get_v3_protocol_fees(pool_id, self.name, self.chains.date_range),
)


@staticmethod
def _get_latest_snapshot(
Expand Down Expand Up @@ -376,7 +422,8 @@ def noncore_fees_collected(self) -> Decimal:
if not self.core_pools:
raise ValueError("core pools not set")
total_core_fees = sum(pool.total_earned_fees_usd_twap for pool in self.core_pools)
return max(self.fees_collected - total_core_fees, Decimal(0))
total_noncore_fees = sum(pool.total_earned_fees_usd_twap for pool in self.alliance_noncore_fee_data)
return max(self.fees_collected - total_core_fees - total_noncore_fees, Decimal(0))

@property
def noncore_to_dao_usd(self) -> Decimal:
Expand All @@ -385,3 +432,15 @@ def noncore_to_dao_usd(self) -> Decimal:
@property
def noncore_to_vebal_usd(self) -> Decimal:
return self.noncore_fees_collected * self.chains.fee_config.noncore_vebal_share_pct

@property
def alliance_noncore_fees_collected(self) -> Decimal:
return sum(pool.total_earned_fees_usd_twap for pool in self.alliance_noncore_fee_data)

@property
def alliance_noncore_to_dao_usd(self) -> Decimal:
return self.alliance_noncore_fees_collected * self.chains.alliance_config.alliance_fee_allocations["non_core"].dao_share_pct

@property
def alliance_noncore_to_vebal_usd(self) -> Decimal:
return self.alliance_noncore_fees_collected * self.chains.alliance_config.alliance_fee_allocations["non_core"].vebal_share_pct
52 changes: 47 additions & 5 deletions fee_allocator/accounting/core_pools.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,13 @@ class PoolFeeData:
last_join_exit_ts: int
bpt_price: Decimal = field(default=Decimal(0))
total_earned_fees_usd_twap: Decimal = None
is_alliance_pool: bool = field(default=False)
is_alliance_non_core_pool: bool = field(default=False)

def __post_init__(self):
if len(self.pool_id) == 42:
# v3 pool; earned fees already calculated
if self.total_earned_fees_usd_twap is None:
if self.total_earned_fees_usd_twap is None:
raise ValueError(f"v3 pool {self.pool_id} must have total_earned_fees_usd_twap set. got {self.total_earned_fees_usd_twap}")
else:
# v2 pool
Expand Down Expand Up @@ -80,43 +82,83 @@ def __init__(self, data: PoolFeeData, chain: CorePoolChain):
self.__dict__.update(vars(data))
self.chain = chain

# Check if this is an Alliance pool
self.is_alliance_pool = self._check_if_alliance_pool()
self.alliance_fee_config = self._get_alliance_fee_config() if self.is_alliance_pool else None
self.is_alliance_non_core_pool = self._is_alliance_non_core_pool()

self.original_earned_fee_share = Decimal(0)
self.earned_fee_share_of_chain_usd = self._earned_fee_share_of_chain_usd()
self.total_to_incentives_usd = self._total_to_incentives_usd()
self.to_aura_incentives_usd = self._to_aura_incentives_usd()
self.to_bal_incentives_usd = self._to_bal_incentives_usd()
self.to_dao_usd = self._to_dao_usd()
self.to_vebal_usd = self._to_vebal_usd()
self.to_partner_usd = self._to_partner_usd() if self.is_alliance_pool else Decimal(0)
self.redirected_incentives_usd = Decimal(0)

override_cls = overrides.get(self.pool_id)
self.override = override_cls(self) if override_cls else None

def _check_if_alliance_pool(self) -> bool:
return self.chain.chains.alliance_config.get_pool_fee_config(self.pool_id, self.chain.name, True) is not None

def _get_alliance_fee_config(self):
return self.chain.chains.alliance_config.get_pool_fee_config(self.pool_id, self.chain.name, True)

def _is_alliance_non_core_pool(self) -> bool:
if not self.is_alliance_pool:
return False

for member in self.chain.chains.alliance_config.alliance_members:
for pool in member.pools:
if pool.pool_type != "core":
print(f"Alliance non-core pool: {pool.pool_id} {pool.network} {pool.active}")
if pool.pool_id == self.pool_id and pool.network == self.chain.name and pool.active:
return pool.pool_type != "core"
return False

def _earned_fee_share_of_chain_usd(self) -> Decimal:
if self.chain.total_earned_fees_usd_twap == 0:
return Decimal(0)
return self.total_earned_fees_usd_twap / self.chain.total_earned_fees_usd_twap

def _total_to_incentives_usd(self) -> Decimal:
to_distribute_to_incentives = self.chain.total_earned_fees_usd_twap * self.chain.chains.fee_config.vote_incentive_pct
to_distribute_to_incentives = self.chain.total_earned_fees_usd_twap * (
self.alliance_fee_config.vote_incentive_pct if self.is_alliance_pool
else self.chain.chains.fee_config.vote_incentive_pct
)
return self.earned_fee_share_of_chain_usd * to_distribute_to_incentives

def _to_aura_incentives_usd(self) -> Decimal:
if self.is_alliance_pool:
return self.total_to_incentives_usd
return self.total_to_incentives_usd * self.chain.chains.aura_vebal_share

def _to_bal_incentives_usd(self) -> Decimal:
if self.is_alliance_pool:
return Decimal(0)
return self.total_to_incentives_usd * (1 - self.chain.chains.aura_vebal_share)

def _to_dao_usd(self) -> Decimal:
return (
self.earned_fee_share_of_chain_usd
* self.chain.total_earned_fees_usd_twap
* self.chain.chains.fee_config.dao_share_pct
* (self.alliance_fee_config.dao_share_pct if self.is_alliance_pool
else self.chain.chains.fee_config.dao_share_pct)
)

def _to_vebal_usd(self) -> Decimal:
return (
self.earned_fee_share_of_chain_usd
* self.chain.total_earned_fees_usd_twap
* self.chain.chains.fee_config.vebal_share_pct
)
* (self.alliance_fee_config.vebal_share_pct if self.is_alliance_pool
else self.chain.chains.fee_config.vebal_share_pct)
)

def _to_partner_usd(self) -> Decimal:
return (
self.earned_fee_share_of_chain_usd
* self.chain.total_earned_fees_usd_twap
* self.alliance_fee_config.partner_share_pct
) if self.is_alliance_pool else Decimal(0)
54 changes: 54 additions & 0 deletions fee_allocator/accounting/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,57 @@ def model_post_init(self, __context):
if any(self.__dict__.values()):
raise ValueError(f"Reroute logic not implemented")


class AlliancePool(BaseModel):
"""
Represents a pool that is part of the Balancer Alliance program.
"""
pool_id: str
network: str
partner: str
pool_type: str
eligibility_date: str
active: bool


class AllianceMember(BaseModel):
"""
Represents a member of the Balancer Alliance program.
"""
name: str
multisig_address: str
active: bool
join_date: str
last_lock_date: str
pools: list[AlliancePool]


class AllianceFeeAllocation(BaseModel):
"""
Represents the fee allocation configuration for Alliance pools.
"""
vebal_share_pct: Decimal
vote_incentive_pct: Decimal | None = None # None for non-core pools
partner_share_pct: Decimal
dao_share_pct: Decimal


class AllianceConfig(BaseModel):
"""
Represents the complete Alliance configuration including members and fee allocations.
Models the data sourced from the ALLIANCE_CONSTANTS_URL endpoint.
"""
alliance_members: list[AllianceMember]
alliance_fee_allocations: dict[str, AllianceFeeAllocation]

def get_pool_fee_config(self, pool_id: str, network: str, is_core: bool) -> AllianceFeeAllocation | None:
"""
Returns the fee allocation configuration for a specific pool if it's part of the Alliance program.
Returns None if the pool is not part of the Alliance program.
"""
for member in self.alliance_members:
for pool in member.pools:
if pool.pool_id == pool_id and pool.network == network and pool.active:
return self.alliance_fee_allocations["core" if is_core else "non_core"]
return None

1 change: 1 addition & 0 deletions fee_allocator/constants.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
FEE_CONSTANTS_URL = "https://raw.githubusercontent.com/BalancerMaxis/multisig-ops/f7e0425b59e474b01d2ede125053238460792630/config/protocol_fees_constants.json"
CORE_POOLS_URL = "https://raw.githubusercontent.com/BalancerMaxis/bal_addresses/main/outputs/core_pools.json"
ALLIANCE_CONFIG_URL = "https://raw.githubusercontent.com/BalancerMaxis/multisig-ops/4246da32541c57fa5f7b920896089fe421c97187/config/alliance_fee_share.json"
REROUTE_CONFIG_URL = "https://raw.githubusercontent.com/BalancerMaxis/multisig-ops/main/config/core_pools_rerouting.json"
POOL_OVERRIDES_URL = "https://raw.githubusercontent.com/BalancerMaxis/multisig-ops/main/config/pool_incentives_overrides.json"
SNAPSHOT_URL = "https://hub.snapshot.org/graphql?"
Expand Down
Loading