Skip to content
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

Csm strategy check #274

Merged
merged 12 commits into from
Oct 18, 2024
Merged
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
76 changes: 67 additions & 9 deletions src/blockchain/deposit_strategy/base_deposit_strategy.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,58 @@
import logging

import variables
from blockchain.deposit_strategy.gas_price_calculator import GasPriceCalculator
from blockchain.deposit_strategy.strategy import DepositStrategy
from blockchain.typings import Web3
from metrics.metrics import DEPOSITABLE_ETHER, POSSIBLE_DEPOSITS_AMOUNT
from metrics.metrics import DEPOSIT_AMOUNT_OK, DEPOSITABLE_ETHER, GAS_FEE, GAS_OK, POSSIBLE_DEPOSITS_AMOUNT
from web3.types import Wei

logger = logging.getLogger(__name__)


class BaseDepositStrategy:
"""
Attributes:
DEPOSITABLE_KEYS_THRESHOLD: If the Staking Module has at least THRESHOLD amount of depositable keys, deposits are allowed
"""
DEPOSITABLE_KEYS_THRESHOLD = 1

def __init__(self, w3: Web3):
class BaseDepositStrategy(DepositStrategy):
def __init__(self, w3: Web3, gas_price_calculator: GasPriceCalculator):
self.w3 = w3
self._gas_price_calculator = gas_price_calculator

def can_deposit_keys_based_on_ether(self, module_id: int) -> bool:
possible_keys = self.deposited_keys_amount(module_id)
success = False
threshold = self._depositable_keys_threshold()
if possible_keys < threshold:
logger.info(
{
'msg': f'Possible deposits amount is {possible_keys}. Skip deposit.',
'module_id': module_id,
'threshold': threshold,
}
)
else:
base_fee_per_gas = self._gas_price_calculator.get_pending_base_fee()
success = self.is_deposit_recommended_based_on_keys_amount(possible_keys, base_fee_per_gas, module_id)
DEPOSIT_AMOUNT_OK.labels(module_id).set(int(success))
return success

def is_gas_price_ok(self, module_id: int) -> bool:
"""
Determines if the gas price is ok for doing a deposit.
"""
current_gas_fee = self._gas_price_calculator.get_pending_base_fee()
GAS_FEE.labels('current_fee', module_id).set(current_gas_fee)

current_buffered_ether = self.w3.lido.lido.get_depositable_ether()
if current_buffered_ether > variables.MAX_BUFFERED_ETHERS:
success = current_gas_fee <= variables.MAX_GAS_FEE
else:
recommended_gas_fee = self._gas_price_calculator.get_recommended_gas_fee()
GAS_FEE.labels('recommended_fee', module_id).set(recommended_gas_fee)
GAS_FEE.labels('max_fee', module_id).set(variables.MAX_GAS_FEE)
success = recommended_gas_fee >= current_gas_fee
GAS_OK.labels(module_id).set(int(success))
return success

def _depositable_keys_threshold(self) -> int:
return 1

def _depositable_ether(self) -> Wei:
depositable_ether = self.w3.lido.lido.get_depositable_ether()
Expand All @@ -31,6 +68,19 @@ def deposited_keys_amount(self, module_id: int) -> int:
POSSIBLE_DEPOSITS_AMOUNT.labels(module_id, 0).set(possible_deposits_amount)
return possible_deposits_amount

def is_deposit_recommended_based_on_keys_amount(self, deposits_amount: int, base_fee: int, module_id: int) -> bool:
return self._recommended_max_gas(deposits_amount, module_id) >= base_fee

@staticmethod
def _recommended_max_gas(deposits_amount: int, module_id: int):
# For one key recommended gas fee will be around 10
# For 10 keys around 100 gwei. For 20 keys ~ 800 gwei
# ToDo percentiles for all modules?
recommended_max_gas = (deposits_amount**3 + 100) * 10**8
logger.info({'msg': 'Calculate recommended max gas based on possible deposits.', 'value': recommended_max_gas})
GAS_FEE.labels('based_on_buffer_fee', module_id).set(recommended_max_gas)
return recommended_max_gas


class MellowDepositStrategy(BaseDepositStrategy):
"""
Expand Down Expand Up @@ -58,3 +108,11 @@ def deposited_keys_amount(self, module_id: int) -> int:
)
POSSIBLE_DEPOSITS_AMOUNT.labels(module_id, 1).set(possible_deposits_amount)
return possible_deposits_amount if possible_deposits_amount_assumption == possible_deposits_amount else 0


class CSMDepositStrategy(BaseDepositStrategy):
def is_deposit_recommended_based_on_keys_amount(self, deposits_amount: int, base_fee: int, module_id: int) -> bool:
return True

def _depositable_keys_threshold(self) -> int:
return 2
55 changes: 2 additions & 53 deletions src/blockchain/deposit_strategy/gas_price_calculator.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,8 @@

import numpy
import variables
from blockchain.deposit_strategy.base_deposit_strategy import BaseDepositStrategy
from blockchain.typings import Web3
from eth_typing import BlockNumber
from metrics.metrics import DEPOSIT_AMOUNT_OK, GAS_FEE, GAS_OK
from web3.types import Wei

logger = logging.getLogger(__name__)
Expand All @@ -21,61 +19,12 @@ class GasPriceCalculator:
def __init__(self, w3: Web3):
self.w3 = w3

def is_gas_price_ok(self, module_id: int) -> bool:
"""
Determines if the gas price is ok for doing a deposit.
"""
current_gas_fee = self._get_pending_base_fee()
GAS_FEE.labels('current_fee', module_id).set(current_gas_fee)

current_buffered_ether = self.w3.lido.lido.get_depositable_ether()
if current_buffered_ether > variables.MAX_BUFFERED_ETHERS:
success = current_gas_fee <= variables.MAX_GAS_FEE
else:
recommended_gas_fee = self._get_recommended_gas_fee()
GAS_FEE.labels('recommended_fee', module_id).set(recommended_gas_fee)
GAS_FEE.labels('max_fee', module_id).set(variables.MAX_GAS_FEE)
success = recommended_gas_fee >= current_gas_fee
GAS_OK.labels(module_id).set(int(success))
return success

def _get_pending_base_fee(self) -> Wei:
def get_pending_base_fee(self) -> Wei:
base_fee_per_gas = self.w3.eth.get_block('pending')['baseFeePerGas']
logger.info({'msg': 'Fetch base_fee_per_gas for pending block.', 'value': base_fee_per_gas})
return base_fee_per_gas

def calculate_deposit_recommendation(self, deposit_strategy: BaseDepositStrategy, module_id: int) -> bool:
possible_keys = deposit_strategy.deposited_keys_amount(module_id)
success = False
if possible_keys < deposit_strategy.DEPOSITABLE_KEYS_THRESHOLD:
logger.info(
{
'msg': f'Possible deposits amount is {possible_keys}. Skip deposit.',
'module_id': module_id,
'threshold': deposit_strategy.DEPOSITABLE_KEYS_THRESHOLD,
}
)
else:
recommended_max_gas = GasPriceCalculator._calculate_recommended_gas_based_on_deposit_amount(
possible_keys,
module_id,
)
base_fee_per_gas = self._get_pending_base_fee()
success = recommended_max_gas >= base_fee_per_gas
DEPOSIT_AMOUNT_OK.labels(module_id).set(int(success))
return success

@staticmethod
def _calculate_recommended_gas_based_on_deposit_amount(deposits_amount: int, module_id: int) -> int:
# For one key recommended gas fee will be around 10
# For 10 keys around 100 gwei. For 20 keys ~ 800 gwei
# ToDo percentiles for all modules?
recommended_max_gas = (deposits_amount ** 3 + 100) * 10 ** 8
logger.info({'msg': 'Calculate recommended max gas based on possible deposits.', 'value': recommended_max_gas})
GAS_FEE.labels('based_on_buffer_fee', module_id).set(recommended_max_gas)
return recommended_max_gas

def _get_recommended_gas_fee(self) -> Wei:
def get_recommended_gas_fee(self) -> Wei:
gas_history = self._fetch_gas_fee_history(variables.GAS_FEE_PERCENTILE_DAYS_HISTORY_1)
return Wei(int(numpy.percentile(gas_history, variables.GAS_FEE_PERCENTILE_1)))

Expand Down
11 changes: 11 additions & 0 deletions src/blockchain/deposit_strategy/strategy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import abc


class DepositStrategy(abc.ABC):
@abc.abstractmethod
def can_deposit_keys_based_on_ether(self, module_id: int) -> bool:
pass

@abc.abstractmethod
def is_gas_price_ok(self, module_id: int) -> bool:
pass
26 changes: 15 additions & 11 deletions src/bots/depositor.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@

import variables
from blockchain.contracts.staking_module import StakingModuleContract
from blockchain.deposit_strategy.base_deposit_strategy import BaseDepositStrategy, MellowDepositStrategy
from blockchain.deposit_strategy.base_deposit_strategy import BaseDepositStrategy, CSMDepositStrategy, MellowDepositStrategy
from blockchain.deposit_strategy.deposit_transaction_sender import Sender
from blockchain.deposit_strategy.gas_price_calculator import GasPriceCalculator
from blockchain.deposit_strategy.prefered_module_to_deposit import get_preferred_to_deposit_modules
from blockchain.deposit_strategy.strategy import DepositStrategy
from blockchain.executor import Executor
from blockchain.typings import Web3
from metrics.metrics import (
Expand Down Expand Up @@ -39,9 +40,10 @@ def run_depositor(w3):
logger.info({'msg': 'Initialize Depositor bot.'})
sender = Sender(w3)
gas_price_calculator = GasPriceCalculator(w3)
mellow_deposit_strategy = MellowDepositStrategy(w3)
base_deposit_strategy = BaseDepositStrategy(w3)
depositor_bot = DepositorBot(w3, sender, gas_price_calculator, mellow_deposit_strategy, base_deposit_strategy)
mellow_deposit_strategy = MellowDepositStrategy(w3, gas_price_calculator)
base_deposit_strategy = BaseDepositStrategy(w3, gas_price_calculator)
csm_strategy = CSMDepositStrategy(w3, gas_price_calculator)
depositor_bot = DepositorBot(w3, sender, mellow_deposit_strategy, base_deposit_strategy, csm_strategy)

e = Executor(
w3,
Expand All @@ -65,15 +67,15 @@ def __init__(
self,
w3: Web3,
sender: Sender,
gas_price_calcaulator: GasPriceCalculator,
mellow_deposit_strategy: MellowDepositStrategy,
base_deposit_strategy: BaseDepositStrategy,
csm_strategy: CSMDepositStrategy,
):
self.w3 = w3
self._sender = sender
self._gas_price_calculator = gas_price_calcaulator
self._mellow_strategy = mellow_deposit_strategy
self._general_strategy = base_deposit_strategy
self._csm_strategy = csm_strategy

transports = []

Expand Down Expand Up @@ -186,17 +188,17 @@ def _deposit_to_module(self, module_id: int) -> bool:
can_deposit = self.w3.lido.deposit_security_module.can_deposit(module_id)
logger.info({'msg': 'Can deposit to module.', 'value': can_deposit})

gas_is_ok = self._gas_price_calculator.is_gas_price_ok(module_id)
strategy, is_mellow = self._select_strategy(module_id)
gas_is_ok = strategy.is_gas_price_ok(module_id)
logger.info({'msg': 'Calculate gas recommendations.', 'value': gas_is_ok})

strategy, is_mellow = self._select_strategy(module_id)
is_deposit_amount_ok = self._gas_price_calculator.calculate_deposit_recommendation(strategy, module_id)
is_deposit_amount_ok = strategy.can_deposit_keys_based_on_ether(module_id)
logger.info({'msg': 'Calculations deposit recommendations.', 'value': is_deposit_amount_ok, 'is_mellow': is_mellow})

if is_mellow and not is_deposit_amount_ok:
strategy = self._general_strategy
is_mellow = False
is_deposit_amount_ok = self._gas_price_calculator.calculate_deposit_recommendation(strategy, module_id)
is_deposit_amount_ok = strategy.can_deposit_keys_based_on_ether(module_id)
logger.info({'msg': 'Calculations deposit recommendations.', 'value': is_deposit_amount_ok, 'is_mellow': is_mellow})

if is_depositable and quorum and can_deposit and gas_is_ok and is_deposit_amount_ok:
Expand All @@ -210,7 +212,9 @@ def _deposit_to_module(self, module_id: int) -> bool:
logger.info({'msg': 'Checks failed. Skip deposit.'})
return False

def _select_strategy(self, module_id) -> tuple[BaseDepositStrategy, bool]:
def _select_strategy(self, module_id) -> tuple[DepositStrategy, bool]:
if module_id == 3:
return self._csm_strategy, False
if self._is_mellow_depositable(module_id):
return self._mellow_strategy, True
return self._general_strategy, False
Expand Down
13 changes: 13 additions & 0 deletions tests/blockchain/deposit_strategy/test_base_deposit_strategy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from unittest.mock import Mock

import pytest


@pytest.mark.unit
def test_csm_deposit_strategy(csm_strategy):
csm_strategy.deposited_keys_amount = Mock(return_value=1)
assert not csm_strategy.can_deposit_keys_based_on_ether(3)

csm_strategy.deposited_keys_amount = Mock(return_value=2)
csm_strategy._gas_price_calculator.get_pending_base_fee = Mock(return_value=10)
assert csm_strategy.can_deposit_keys_based_on_ether(3)
36 changes: 18 additions & 18 deletions tests/blockchain/deposit_strategy/test_gas_price_calculator.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,38 +2,38 @@

import pytest
import variables
from blockchain.deposit_strategy.base_deposit_strategy import BaseDepositStrategy

MODULE_ID = 1


@pytest.mark.unit
def test_is_gas_price_ok(gas_price_calculator):
gas_price_calculator._get_pending_base_fee = Mock(return_value=10)
gas_price_calculator._get_recommended_gas_fee = Mock(return_value=20)
def test_is_gas_price_ok(base_deposit_strategy):
base_deposit_strategy._gas_price_calculator.get_pending_base_fee = Mock(return_value=10)
base_deposit_strategy._gas_price_calculator.get_recommended_gas_fee = Mock(return_value=20)
variables.MAX_GAS_FEE = 300

gas_price_calculator.w3.lido.lido.get_depositable_ether = Mock(return_value=100)
base_deposit_strategy._gas_price_calculator.w3.lido.lido.get_depositable_ether = Mock(return_value=100)
variables.MAX_BUFFERED_ETHERS = 200
assert gas_price_calculator.is_gas_price_ok(MODULE_ID)
assert base_deposit_strategy.is_gas_price_ok(MODULE_ID)

gas_price_calculator._get_recommended_gas_fee = Mock(return_value=5)
assert not gas_price_calculator.is_gas_price_ok(MODULE_ID)
base_deposit_strategy._gas_price_calculator.get_recommended_gas_fee = Mock(return_value=5)
assert not base_deposit_strategy.is_gas_price_ok(MODULE_ID)

gas_price_calculator.w3.lido.lido.get_depositable_ether = Mock(return_value=300)
assert gas_price_calculator.is_gas_price_ok(MODULE_ID)
base_deposit_strategy._gas_price_calculator.w3.lido.lido.get_depositable_ether = Mock(return_value=300)
assert base_deposit_strategy.is_gas_price_ok(MODULE_ID)

gas_price_calculator._get_pending_base_fee = Mock(return_value=400)
assert not gas_price_calculator.is_gas_price_ok(MODULE_ID)
base_deposit_strategy._gas_price_calculator.get_pending_base_fee = Mock(return_value=400)
assert not base_deposit_strategy.is_gas_price_ok(MODULE_ID)


@pytest.mark.unit
@pytest.mark.parametrize(
'deposits,expected_range',
[(1, (0, 20)), (5, (20, 100)), (10, (50, 1000)), (100, (1000, 1000000))],
)
def test_calculate_recommended_gas_based_on_deposit_amount(gas_price_calculator, deposits, expected_range):
assert expected_range[0] * 10 ** 9 <= gas_price_calculator._calculate_recommended_gas_based_on_deposit_amount(deposits, MODULE_ID) <= \
expected_range[1] * 10 ** 9
def test_calculate_recommended_gas_based_on_deposit_amount(deposits, expected_range):
assert expected_range[0] * 10**9 <= BaseDepositStrategy._recommended_max_gas(deposits, MODULE_ID) <= expected_range[1] * 10**9


@pytest.mark.unit
Expand All @@ -42,16 +42,16 @@ def test_get_recommended_gas_fee(gas_price_calculator):
variables.GAS_FEE_PERCENTILE_DAYS_HISTORY_1 = 1
variables.GAS_FEE_PERCENTILE_1 = 50

assert gas_price_calculator._get_recommended_gas_fee() == 5
assert gas_price_calculator.get_recommended_gas_fee() == 5

variables.GAS_FEE_PERCENTILE_1 = 30
assert gas_price_calculator._get_recommended_gas_fee() == 3
assert gas_price_calculator.get_recommended_gas_fee() == 3


@pytest.mark.integration
def test_get_pending_base_fee(gas_price_calculator_integration):
pending_gas = gas_price_calculator_integration._get_pending_base_fee()
assert 1 <= pending_gas <= 1000 * 10 ** 9
pending_gas = gas_price_calculator_integration.get_pending_base_fee()
assert 1 <= pending_gas <= 1000 * 10**9


@pytest.mark.integration
Expand Down
Loading
Loading