Skip to content

Commit

Permalink
Merge pull request #275 from lidofinance/develop
Browse files Browse the repository at this point in the history
Depositor bot release (#274)
  • Loading branch information
F4ever authored Oct 21, 2024
2 parents 944f3aa + a180c7e commit 74b6432
Show file tree
Hide file tree
Showing 10 changed files with 160 additions and 107 deletions.
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

0 comments on commit 74b6432

Please sign in to comment.