diff --git a/.gitignore b/.gitignore index 7d92e4b848..d096cc8ab7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .env .idea/ +.vscode/ .yarn/ node_modules/ diff --git a/backend/app/controllers/rewards.py b/backend/app/controllers/rewards.py index 1e2a728dfb..fe911bc9fe 100644 --- a/backend/app/controllers/rewards.py +++ b/backend/app/controllers/rewards.py @@ -6,11 +6,15 @@ from dataclass_wizard import JSONWizard +from app.core.epochs import epoch_snapshots as core_epoch_snapshots from app import database from app import exceptions -from app.core import proposals, merkle_tree, epochs as core_epochs +from app.core import proposals, merkle_tree from app.core.proposals import get_proposals_with_allocations -from app.core.rewards import calculate_matched_rewards, get_matched_rewards_from_epoch +from app.core.rewards.rewards import ( + calculate_matched_rewards, + get_matched_rewards_from_epoch, +) from app.extensions import epochs @@ -92,7 +96,7 @@ def get_proposals_rewards(epoch: int = None) -> List[ProposalReward]: def get_rewards_merkle_tree(epoch: int) -> RewardsMerkleTree: - if not core_epochs.has_finalized_epoch_snapshot(epoch): + if not core_epoch_snapshots.has_finalized_epoch_snapshot(epoch): raise exceptions.InvalidEpoch mt = merkle_tree.get_merkle_tree_for_epoch(epoch) diff --git a/backend/app/controllers/snapshots.py b/backend/app/controllers/snapshots.py index af3a25d34f..dc88f3971f 100644 --- a/backend/app/controllers/snapshots.py +++ b/backend/app/controllers/snapshots.py @@ -7,9 +7,15 @@ from app import exceptions, database from app.core import glm, user as user_core, merkle_tree from app.core.deposits.deposits import get_users_deposits, calculate_locked_ratio -from app.core.epochs import has_pending_epoch_snapshot, has_finalized_epoch_snapshot +from app.core.epochs.epoch_snapshots import ( + has_pending_epoch_snapshot, + has_finalized_epoch_snapshot, +) from app.core.proposals import get_proposal_rewards_above_threshold -from app.core.rewards import calculate_total_rewards, calculate_all_individual_rewards +from app.core.rewards.rewards import ( + calculate_total_rewards, + calculate_all_individual_rewards, +) from app.database import pending_epoch_snapshot, finalized_epoch_snapshot from app.extensions import db, w3, epochs @@ -44,9 +50,9 @@ def snapshot_pending_epoch() -> Optional[int]: eth_proceeds = w3.eth.get_balance(app.config["WITHDRAWALS_TARGET_CONTRACT_ADDRESS"]) user_deposits, total_effective_deposit = get_users_deposits(pending_epoch) locked_ratio = calculate_locked_ratio(total_effective_deposit, glm_supply) - total_rewards = calculate_total_rewards(eth_proceeds, locked_ratio) + total_rewards = calculate_total_rewards(eth_proceeds, locked_ratio, pending_epoch) all_individual_rewards = calculate_all_individual_rewards( - eth_proceeds, locked_ratio + eth_proceeds, locked_ratio, pending_epoch ) database.deposits.add_all(pending_epoch, user_deposits) diff --git a/backend/app/core/allocations.py b/backend/app/core/allocations.py index e92d79c700..7688c2862c 100644 --- a/backend/app/core/allocations.py +++ b/backend/app/core/allocations.py @@ -4,7 +4,7 @@ from dataclass_wizard import JSONWizard from app import database, exceptions -from app.core.epochs import has_pending_epoch_snapshot +from app.core.epochs.epoch_snapshots import has_pending_epoch_snapshot from app.core.user import get_budget from app.crypto.eip712 import recover_address, build_allocations_eip712_data from app.extensions import proposals diff --git a/backend/app/core/epochs/__init__.py b/backend/app/core/epochs/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/backend/app/core/epochs.py b/backend/app/core/epochs/epoch_snapshots.py similarity index 100% rename from backend/app/core/epochs.py rename to backend/app/core/epochs/epoch_snapshots.py diff --git a/backend/app/core/epochs/epochs_registry.py b/backend/app/core/epochs/epochs_registry.py new file mode 100644 index 0000000000..35e2ef8f89 --- /dev/null +++ b/backend/app/core/epochs/epochs_registry.py @@ -0,0 +1,23 @@ +from dataclasses import dataclass + +from app.core.rewards.rewards_strategy import RewardsStrategy +from app.core.rewards.standard_rewards_strategy import StandardRewardsStrategy + + +@dataclass(frozen=True) +class EpochSettings: + rewardsStrategy: RewardsStrategy = StandardRewardsStrategy() + + +class EpochsRegistry: + rewards_registry: dict[int, EpochSettings] = {} + + @classmethod + def register_epoch_settings( + cls, epoch_number: int, rewardsStrategy: RewardsStrategy + ): + cls.rewards_registry[epoch_number] = EpochSettings(rewardsStrategy) + + @classmethod + def get_epoch_settings(cls, epoch_number: int) -> EpochSettings: + return cls.rewards_registry.setdefault(epoch_number, EpochSettings()) diff --git a/backend/app/core/proposals.py b/backend/app/core/proposals.py index 1e72aee6f7..d258b45cdb 100644 --- a/backend/app/core/proposals.py +++ b/backend/app/core/proposals.py @@ -4,7 +4,7 @@ from app import database from app.core.common import AccountFunds -from app.core.rewards import ( +from app.core.rewards.rewards import ( get_matched_rewards_from_epoch, calculate_matched_rewards_threshold, ) diff --git a/backend/app/core/rewards.py b/backend/app/core/rewards.py deleted file mode 100644 index 32b4e3d7dd..0000000000 --- a/backend/app/core/rewards.py +++ /dev/null @@ -1,28 +0,0 @@ -from decimal import Decimal - -from app import database -from app.database.models import PendingEpochSnapshot - - -def calculate_total_rewards(eth_proceeds: int, locked_ratio: Decimal) -> int: - return int(eth_proceeds * locked_ratio.sqrt()) - - -def calculate_all_individual_rewards(eth_proceeds: int, locked_ratio: Decimal) -> int: - return int(eth_proceeds * locked_ratio) - - -def calculate_matched_rewards(snapshot: PendingEpochSnapshot) -> int: - return int(snapshot.total_rewards) - int(snapshot.all_individual_rewards) - - -def get_matched_rewards_from_epoch(epoch: int) -> int: - snapshot = database.pending_epoch_snapshot.get_by_epoch_num(epoch) - - return calculate_matched_rewards(snapshot) - - -def calculate_matched_rewards_threshold( - total_allocated: int, proposals_count: int -) -> int: - return int(total_allocated / (proposals_count * 2)) diff --git a/backend/app/core/rewards/__init__.py b/backend/app/core/rewards/__init__.py new file mode 100644 index 0000000000..e43c818681 --- /dev/null +++ b/backend/app/core/rewards/__init__.py @@ -0,0 +1,4 @@ +from app.core.epochs.epochs_registry import EpochsRegistry +from app.core.rewards.double_rewards_strategy import DoubleRewardsStrategy + +EpochsRegistry.register_epoch_settings(1, DoubleRewardsStrategy()) diff --git a/backend/app/core/rewards/double_rewards_strategy.py b/backend/app/core/rewards/double_rewards_strategy.py new file mode 100644 index 0000000000..6c249df67f --- /dev/null +++ b/backend/app/core/rewards/double_rewards_strategy.py @@ -0,0 +1,24 @@ +from decimal import Decimal +from app.core.rewards.rewards_strategy import RewardsStrategy + + +class DoubleRewardsStrategy(RewardsStrategy): + DOUBLING_GLM_SUPPLY_LIMIT = 0.25 + REWARDS_MULTIPLY_RATIO_LIMIT = 0.5 + REWARDS_MULTIPLY_FACTOR = 2 + + def calculate_total_rewards(self, eth_proceeds: int, locked_ratio: Decimal) -> int: + if locked_ratio < self.DOUBLING_GLM_SUPPLY_LIMIT: + return ( + int(eth_proceeds * locked_ratio.sqrt()) * self.REWARDS_MULTIPLY_FACTOR + ) + else: + return eth_proceeds + + def calculate_all_individual_rewards( + self, eth_proceeds: int, locked_ratio: Decimal + ) -> int: + if locked_ratio < self.DOUBLING_GLM_SUPPLY_LIMIT: + return int(eth_proceeds * locked_ratio) * self.REWARDS_MULTIPLY_FACTOR + else: + return int(eth_proceeds * self.REWARDS_MULTIPLY_RATIO_LIMIT) diff --git a/backend/app/core/rewards/rewards.py b/backend/app/core/rewards/rewards.py new file mode 100644 index 0000000000..e93146f398 --- /dev/null +++ b/backend/app/core/rewards/rewards.py @@ -0,0 +1,38 @@ +from decimal import Decimal + +from app import database +from app.database.models import PendingEpochSnapshot + +from app.core.epochs.epochs_registry import EpochsRegistry + + +def calculate_total_rewards( + eth_proceeds: int, locked_ratio: Decimal, pending_epoch: int +) -> int: + registry = EpochsRegistry.get_epoch_settings(pending_epoch) + return registry.rewardsStrategy.calculate_total_rewards(eth_proceeds, locked_ratio) + + +def calculate_all_individual_rewards( + eth_proceeds: int, locked_ratio: Decimal, pending_epoch: int +) -> int: + registry = EpochsRegistry.get_epoch_settings(pending_epoch) + return registry.rewardsStrategy.calculate_all_individual_rewards( + eth_proceeds, locked_ratio + ) + + +def calculate_matched_rewards(snapshot: PendingEpochSnapshot) -> int: + return int(snapshot.total_rewards) - int(snapshot.all_individual_rewards) + + +def get_matched_rewards_from_epoch(epoch: int) -> int: + snapshot = database.pending_epoch_snapshot.get_by_epoch_num(epoch) + + return calculate_matched_rewards(snapshot) + + +def calculate_matched_rewards_threshold( + total_allocated: int, proposals_count: int +) -> int: + return int(total_allocated / (proposals_count * 2)) diff --git a/backend/app/core/rewards/rewards_strategy.py b/backend/app/core/rewards/rewards_strategy.py new file mode 100644 index 0000000000..0675861cd7 --- /dev/null +++ b/backend/app/core/rewards/rewards_strategy.py @@ -0,0 +1,14 @@ +from decimal import Decimal +from abc import ABC, abstractmethod + + +class RewardsStrategy(ABC): + @abstractmethod + def calculate_total_rewards(self, eth_proceeds: int, locked_ratio: Decimal) -> int: + pass + + @abstractmethod + def calculate_all_individual_rewards( + self, eth_proceeds: int, locked_ratio: Decimal + ) -> int: + pass diff --git a/backend/app/core/rewards/standard_rewards_strategy.py b/backend/app/core/rewards/standard_rewards_strategy.py new file mode 100644 index 0000000000..871c8c70a6 --- /dev/null +++ b/backend/app/core/rewards/standard_rewards_strategy.py @@ -0,0 +1,12 @@ +from decimal import Decimal +from app.core.rewards.rewards_strategy import RewardsStrategy + + +class StandardRewardsStrategy(RewardsStrategy): + def calculate_total_rewards(self, eth_proceeds: int, locked_ratio: Decimal) -> int: + return int(eth_proceeds * locked_ratio.sqrt()) + + def calculate_all_individual_rewards( + self, eth_proceeds: int, locked_ratio: Decimal + ) -> int: + return int(eth_proceeds * locked_ratio) diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index 0c84de04fd..972d15af6c 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -16,7 +16,7 @@ from app.contracts.vault import Vault from app.controllers.allocations import allocate from app.core.allocations import AllocationRequest, Allocation -from app.core.rewards import calculate_matched_rewards +from app.core.rewards.rewards import calculate_matched_rewards from app.crypto.eip712 import sign, build_allocations_eip712_data from app.extensions import db, w3 from app.settings import TestConfig diff --git a/backend/tests/test_allocations.py b/backend/tests/test_allocations.py index 5d8fc47c2a..6e6ade977b 100644 --- a/backend/tests/test_allocations.py +++ b/backend/tests/test_allocations.py @@ -14,7 +14,7 @@ AllocationRequest, Allocation, ) -from app.core.rewards import calculate_matched_rewards_threshold +from app.core.rewards.rewards import calculate_matched_rewards_threshold from app.crypto.eip712 import sign, build_allocations_eip712_data from app.extensions import db from tests.conftest import ( diff --git a/backend/tests/test_rewards.py b/backend/tests/test_rewards.py index e9752582d3..104bf9f705 100644 --- a/backend/tests/test_rewards.py +++ b/backend/tests/test_rewards.py @@ -13,7 +13,7 @@ from app.core.allocations import ( AllocationRequest, ) -from app.core.rewards import ( +from app.core.rewards.rewards import ( calculate_total_rewards, calculate_all_individual_rewards, get_matched_rewards_from_epoch, @@ -43,30 +43,66 @@ def before( @pytest.mark.parametrize( - "eth_proceeds,locked_ratio,expected", + "eth_proceeds,locked_ratio,pending_epoch,expected", [ - (4_338473610_477382755, Decimal("0.0000004"), 2743891_635528535), - (600_000000000_000000000, Decimal("0.0003298799699"), 10_897558862_607717064), - (10_000000000_000000000, Decimal("0.43"), 6_557438524_302000652), - (1200_000000000_000000000, Decimal("1"), 1200_000000000_000000000), + (4_338473610_477382755, Decimal("0.0000004"), 1, 5487783_271057070), + (4_338473610_477382755, Decimal("0.0000004"), 2, 2743891_635528535), + ( + 600_000000000_000000000, + Decimal("0.0003298799699"), + 1, + 21_795117725_215434128, + ), + ( + 600_000000000_000000000, + Decimal("0.0003298799699"), + 2, + 10_897558862_607717064, + ), + (10_000000000_000000000, Decimal("0.2"), 1, 8_944271909_999158784), + (10_000000000_000000000, Decimal("0.2"), 2, 4472135954999579392), + (10_000000000_000000000, Decimal("0.2"), 3, 4472135954999579392), + (10_000000000_000000000, Decimal("0.25"), 1, 10_000000000_000000000), + (10_000000000_000000000, Decimal("0.25"), 2, 5_000000000_000000000), + (10_000000000_000000000, Decimal("0.25"), 3, 5_000000000_000000000), + (10_000000000_000000000, Decimal("0.43"), 1, 10_000000000_000000000), + (10_000000000_000000000, Decimal("0.43"), 2, 6_557438524_302000652), + (10_000000000_000000000, Decimal("0.43"), 3, 6_557438524_302000652), + (1200_000000000_000000000, Decimal("1"), 1, 1200_000000000_000000000), + (1200_000000000_000000000, Decimal("1"), 2, 1200_000000000_000000000), + (1200_000000000_000000000, Decimal("1"), 3, 1200_000000000_000000000), ], ) -def test_calculate_total_rewards(eth_proceeds, locked_ratio, expected): - result = calculate_total_rewards(eth_proceeds, locked_ratio) +def test_calculate_total_rewards(eth_proceeds, locked_ratio, pending_epoch, expected): + result = calculate_total_rewards(eth_proceeds, locked_ratio, pending_epoch) assert result == expected @pytest.mark.parametrize( - "eth_proceeds,locked_ratio,expected", + "eth_proceeds,locked_ratio,pending_epoch,expected", [ - (4_338473610_477382755, Decimal("0.0000004"), 1735_389444190), - (600_000000000_000000000, Decimal("0.0003298799699"), 197927981_940000000), - (10_000000000_000000000, Decimal("0.43"), 4_300000000_000000000), - (1200_000000000_000000000, Decimal("1"), 1200_000000000_000000000), + (4_338473610_477382755, Decimal("0.0000004"), 1, 3470_778888380), + (4_338473610_477382755, Decimal("0.0000004"), 2, 1735_389444190), + (600_000000000_000000000, Decimal("0.0003298799699"), 1, 395855963_880000000), + (600_000000000_000000000, Decimal("0.0003298799699"), 2, 197927981_940000000), + (10_000000000_000000000, Decimal("0.2"), 1, 4_000000000_000000000), + (10_000000000_000000000, Decimal("0.2"), 2, 2_000000000_000000000), + (10_000000000_000000000, Decimal("0.2"), 3, 2_000000000_000000000), + (10_000000000_000000000, Decimal("0.25"), 1, 5_000000000_000000000), + (10_000000000_000000000, Decimal("0.25"), 2, 2_500000000_000000000), + (10_000000000_000000000, Decimal("0.25"), 3, 2_500000000_000000000), + (10_000000000_000000000, Decimal("0.43"), 1, 5_000000000_000000000), + (10_000000000_000000000, Decimal("0.43"), 2, 4_300000000_000000000), + (10_000000000_000000000, Decimal("0.43"), 3, 4_300000000_000000000), + (1200_000000000_000000000, Decimal("1"), 1, 600_000000000_000000000), + (1200_000000000_000000000, Decimal("1"), 2, 1200_000000000_000000000), + (1200_000000000_000000000, Decimal("1"), 3, 1200_000000000_000000000), ], ) -def test_calculate_all_individual_rewards(eth_proceeds, locked_ratio, expected): - result = calculate_all_individual_rewards(eth_proceeds, locked_ratio) +def test_calculate_all_individual_rewards( + eth_proceeds, locked_ratio, pending_epoch, expected +): + result = calculate_all_individual_rewards(eth_proceeds, locked_ratio, pending_epoch) assert result == expected