Skip to content

Commit

Permalink
Add buffers monitoring test (#86)
Browse files Browse the repository at this point in the history
Bringing back the PR #75 after temporarily reverting it in PR #85 as,
for some reason, there were some formatting issues and the PR still
managed to get merged.

---------

Co-authored-by: Felix Henneke <felix.henneke@protonmail.com>
  • Loading branch information
harisang and fhenneke authored Nov 9, 2023
1 parent 7f9deab commit 522cabd
Show file tree
Hide file tree
Showing 5 changed files with 202 additions and 0 deletions.
45 changes: 45 additions & 0 deletions src/apis/coingeckoapi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""
CoingeckoAPI for fetching the price in usd of a given token.
"""
# pylint: disable=logging-fstring-interpolation

from typing import Optional
import requests
from src.helper_functions import get_logger
from src.constants import (
header,
REQUEST_TIMEOUT,
)


class CoingeckoAPI:
"""
Class for fetching token prices from Coingecko.
"""

def __init__(self) -> None:
self.logger = get_logger()

def get_token_price_in_usd(self, address: str) -> Optional[float]:
"""
Returns the Coingecko price in usd of the given token.
"""
coingecko_url = (
"https://api.coingecko.com/api/v3/simple/token_price/ethereum?contract_addresses="
+ address
+ "&vs_currencies=usd"
)
try:
coingecko_data = requests.get(
coingecko_url,
headers=header,
timeout=REQUEST_TIMEOUT,
)
coingecko_rsp = coingecko_data.json()
coingecko_price_in_usd = float(coingecko_rsp[address]["usd"])
except requests.RequestException as err:
self.logger.warning(
f"Connection error while fetching Coingecko price for token {address}, error: {err}"
)
return None
return coingecko_price_in_usd
47 changes: 47 additions & 0 deletions src/apis/tokenlistapi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"""
TokenListAPI for fetching a curated token list.
"""
# pylint: disable=logging-fstring-interpolation
from typing import Optional
import requests
from src.helper_functions import get_logger
from src.constants import (
header,
REQUEST_TIMEOUT,
)


class TokenListAPI:
"""
Class for fetching a curated token list.
"""

def __init__(self) -> None:
self.logger = get_logger()
self.token_lists = [
"http://t2crtokens.eth.link",
"https://tokens.1inch.eth.link",
"https://tokenlist.aave.eth.link",
]

def get_token_list(self) -> Optional[list[str]]:
"""
Returns a token list.
"""
token_list: list[str] = []
for url in self.token_lists:
try:
data = requests.get(
url,
headers=header,
timeout=REQUEST_TIMEOUT,
)
rsp = data.json()
if "tokens" in rsp:
for token in rsp["tokens"]:
token_list.append(token["address"].lower())
except requests.RequestException as err:
self.logger.warning(f"Exception while fetching a token list: {err}")
if len(token_list) > 0:
return token_list
return None
6 changes: 6 additions & 0 deletions src/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@
# cap parameter, per CIP-20, measured in ETH
CAP_PARAMETER = 0.01

# number of tx hashes before a new buffers check is ran
BUFFER_INTERVAL = 150

# threshold of value of buffers above which an alert is generated
BUFFERS_VALUE_USD_THRESHOLD = 200000

# threshold parameter to generate an alert when receiving kickbacks
KICKBACKS_ALERT_THRESHOLD = 0.03

Expand Down
4 changes: 4 additions & 0 deletions src/daemon.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@
from src.monitoring_tests.mev_blocker_kickbacks_test import (
MEVBlockerRefundsMonitoringTest,
)
from src.monitoring_tests.buffers_monitoring_test import (
BuffersMonitoringTest,
)
from src.constants import SLEEP_TIME_IN_SEC


Expand All @@ -44,6 +47,7 @@ def main() -> None:
PartialFillCostCoverageTest(),
CostCoveragePerSolverTest(),
MEVBlockerRefundsMonitoringTest(),
BuffersMonitoringTest(),
]

start_block: Optional[int] = None
Expand Down
100 changes: 100 additions & 0 deletions src/monitoring_tests/buffers_monitoring_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
"""
Checks the value of buffers every 150 settlements by invoking
the ehtplorer api, and in some cases, coingecko.
"""
# pylint: disable=logging-fstring-interpolation
import requests
from src.monitoring_tests.base_test import BaseTest
from src.apis.coingeckoapi import CoingeckoAPI
from src.apis.tokenlistapi import TokenListAPI
from src.constants import (
BUFFER_INTERVAL,
header,
REQUEST_TIMEOUT,
BUFFERS_VALUE_USD_THRESHOLD,
)


class BuffersMonitoringTest(BaseTest):
"""
This test checks the value of the settlement contract buffers
every 150 settlements and generates an alert if it is higher than 200_000 USD.
Price feeds from ethplorer and coingecko (as backup) are used.
"""

def __init__(self) -> None:
super().__init__()
self.coingecko_api = CoingeckoAPI()
self.tokenlist_api = TokenListAPI()
self.counter: int = 0

def compute_buffers_value(self) -> bool:
"""
Evaluates current state of buffers.
"""
# get all token balances of the smart contract
try:
ethplorer_data = requests.get(
"https://api.ethplorer.io/getAddressInfo/"
+ "0x9008D19f58AAbD9eD0D60971565AA8510560ab41?apiKey=freekey",
headers=header,
timeout=REQUEST_TIMEOUT,
)
ethplorer_rsp = ethplorer_data.json()
if "tokens" not in ethplorer_rsp:
return False
token_list = self.tokenlist_api.get_token_list()
if token_list is None:
return False

value_in_usd = 0.0
for token in ethplorer_rsp["tokens"]:
if token["tokenInfo"]["address"] not in token_list:
continue
balance = token["balance"]
decimals = int(token["tokenInfo"]["decimals"])
if token["tokenInfo"]["price"] is not False:
price_in_usd = token["tokenInfo"]["price"]["rate"]
token_buffer_value_in_usd = (
balance / 10**decimals
) * price_in_usd
# in case some price is way off and it blows a lot the total value held in the
# smart contract we use a second price feed, from coingecko, to correct in case
# the initial price is indeed off
if token_buffer_value_in_usd > 10000:
coingecko_price_in_usd = (
self.coingecko_api.get_token_price_in_usd(
token["tokenInfo"]["address"]
)
)
coingecko_value_in_usd = (
balance / 10**decimals
) * coingecko_price_in_usd
if coingecko_value_in_usd < token_buffer_value_in_usd:
token_buffer_value_in_usd = coingecko_value_in_usd
value_in_usd += token_buffer_value_in_usd
log_output = f"Buffer value is {value_in_usd} USD"
if value_in_usd > BUFFERS_VALUE_USD_THRESHOLD:
self.alert(log_output)
else:
self.logger.info(log_output)

except requests.RequestException as err:
self.logger.warning(
f"Connection Error while fetching buffer tokens and prices, error: {err}"
)
return False
return True

def run(self, tx_hash: str) -> bool:
"""
Wrapper function for the whole test. Checks if BUFFER_INTERVAL many settlements have
been observed, in which case it invokes the main function that checks the current
value of buffers.
"""
self.counter += 1
if self.counter > BUFFER_INTERVAL:
success = self.compute_buffers_value()
if success:
self.counter = 0
return True

0 comments on commit 522cabd

Please sign in to comment.