Skip to content
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
86 changes: 59 additions & 27 deletions df_py/predictoor/calc_rewards.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,47 @@
from df_py.predictoor.queries import query_predictoor_contracts
from df_py.util.graphutil import wait_to_latest_block

WEEK_SECONDS = 7 * 24 * 60 * 60


@enforce_types
def calc_predictoor_rewards(
predictoors: Dict[str, Predictoor], tokens_avail: Union[int, float], chain_id: int
) -> Dict[str, Dict[str, float]]:
"""
Calculate rewards for predictoors based on their weekly payout.
Calculate rewards for predictoors, distributed per epoch (slot).

The budget is split in three stages:
1. equally across prediction feeds (contracts),
2. equally across all possible weekly epochs (slots) for that feed,
3. within each epoch, proportionally to each predictoor's positive
profit (payout - stake) for that epoch.

Splitting by epoch first bounds how much a single position can capture:
one large prediction in one epoch can win at most that epoch's small
slice of the budget, not the whole weekly feed budget. This makes
"both-siding" across unlinkable wallets far less profitable, since the
manufactured-profit wallet can only ever drain a per-epoch budget.

@arguments
predictoors -- dict of [pdr_address] : Predictoor objects
The predictoors to calculate rewards for.
tokens_avail -- float
The number of tokens available for distribution as rewards.
chain_id -- int
The chain to query the available feeds (contracts) from.

@return
rewards -- dict of [contract addr][predictoor addr]: float
The calculated rewards for each predictoor per contract address.
The calculated rewards for each predictoor per contract address,
aggregated across all epochs of that contract.
"""
MIN_REWARD = 1e-15
tokens_avail = float(tokens_avail)

wait_to_latest_block(chain_id)

predictoor_contracts = query_predictoor_contracts(chain_id).keys()
predictoor_contracts = query_predictoor_contracts(chain_id)
print("# of available contracts: ", len(predictoor_contracts))
tokens_per_contract = tokens_avail / len(predictoor_contracts)
print("Tokens per contract:", tokens_per_contract)
Expand All @@ -39,33 +56,48 @@ def calc_predictoor_rewards(
contract: {} for contract in predictoor_contracts
}

# Loop through each contract and calculate the rewards for predictions
# made for that specific contract
for contract in predictoor_contracts:
total_revenue_for_contract = 0
for p in predictoors.values():
summary = p.get_prediction_summary(contract)
total_revenue_for_contract += max(
summary.total_revenue, 0
) # ignore negative values

# If total revenue for this contract is 0, no rewards are distributed
if total_revenue_for_contract == 0:
print("Total revenue for contract: ", contract, " was zero")
for contract, contract_obj in predictoor_contracts.items():
# Build per-epoch profits for this contract:
# epoch_profits[slot][pdr_address] = summed profit for that epoch
epoch_profits: Dict[int, Dict[str, float]] = {}
for pdr_address, predictoor in predictoors.items():
for prediction in predictoor._predictions:
if prediction.contract_addr != contract:
continue
slot_profits = epoch_profits.setdefault(prediction.slot, {})
slot_profits[pdr_address] = (
slot_profits.get(pdr_address, 0.0) + prediction.revenue
)

seconds_per_epoch = int(contract_obj.blocks_per_epoch)
num_epochs = int(WEEK_SECONDS / seconds_per_epoch)
if num_epochs == 0:
print("No epochs for contract: ", contract)
continue

# Calculate rewards for each predictoor for this contract
for pdr_address, predictoor in predictoors.items():
revenue_contract = predictoor.get_prediction_summary(contract).total_revenue
if revenue_contract <= 0:
# ignore negative revenues
continue
reward_amt = (
revenue_contract / total_revenue_for_contract * tokens_per_contract
)
if reward_amt < MIN_REWARD:
# Each epoch gets an equal slice of this contract's budget.
epoch_budget = tokens_per_contract / num_epochs

for slot_profits in epoch_profits.values():
total_positive = sum(max(p, 0.0) for p in slot_profits.values())

# If nobody profited this epoch, its budget is not distributed.
if total_positive == 0:
continue
rewards[contract][pdr_address] = reward_amt

for pdr_address, profit in slot_profits.items():
if profit <= 0:
# ignore non-positive (losing) profits
continue
reward_amt = profit / total_positive * epoch_budget
rewards[contract][pdr_address] = (
rewards[contract].get(pdr_address, 0.0) + reward_amt
)

# drop dust amounts
rewards[contract] = {
addr: amt for addr, amt in rewards[contract].items() if amt >= MIN_REWARD
}

return rewards

Expand Down
48 changes: 47 additions & 1 deletion df_py/predictoor/test/test_predictoor_calc_rewards.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import pytest

from df_py.predictoor.calc_rewards import (
WEEK_SECONDS,
aggregate_predictoor_rewards,
calc_predictoor_rewards,
)
Expand All @@ -13,10 +14,18 @@
from df_py.util.reward_shaper import RewardShaper


class MockPredictContract:
def __init__(self, seconds_per_epoch):
self.blocks_per_epoch = seconds_per_epoch


@pytest.fixture(autouse=True)
def mock_query_functions():
with patch("df_py.predictoor.calc_rewards.query_predictoor_contracts") as mock:
mock.return_value = {"0xContract1": "", "0xContract2": ""}
mock.return_value = {
"0xContract1": MockPredictContract(WEEK_SECONDS),
"0xContract2": MockPredictContract(WEEK_SECONDS),
}
yield


Expand Down Expand Up @@ -130,6 +139,43 @@ def test_reward_calculation_with_negative():
assert len(rewards["0xContract2"]) == 0


def test_epoch_based_rewards_bound_single_epoch_capture():
with patch("df_py.predictoor.calc_rewards.query_predictoor_contracts") as mock:
mock.return_value = {
# 10 possible weekly epochs. A predictoor active in one epoch can
# win at most 1/10 of this contract's budget.
"0xContract1": MockPredictContract(WEEK_SECONDS // 10),
}

whale = Predictoor("0x1")
whale.add_prediction(Prediction(1, 10000.0, 0.0, "0xContract1"))

rewards = calc_predictoor_rewards({"0x1": whale}, 100, DEV_CHAINID)

assert rewards["0xContract1"]["0x1"] == 10.0


def test_epoch_based_rewards_do_not_compare_profit_across_epochs():
with patch("df_py.predictoor.calc_rewards.query_predictoor_contracts") as mock:
mock.return_value = {
# Two possible weekly epochs, so each epoch has 50 tokens.
"0xContract1": MockPredictContract(WEEK_SECONDS // 2),
}

whale = Predictoor("0x1")
whale.add_prediction(Prediction(1, 10000.0, 0.0, "0xContract1"))

small = Predictoor("0x2")
small.add_prediction(Prediction(2, 1.0, 0.0, "0xContract1"))

rewards = calc_predictoor_rewards(
{"0x1": whale, "0x2": small}, 100, DEV_CHAINID
)

assert rewards["0xContract1"]["0x1"] == 50.0
assert rewards["0xContract1"]["0x2"] == 50.0


def test_calc_predictoor_rewards_fuzz():
predictoors = {}
for i in range(100): # generate 100 predictoors
Expand Down
Loading