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

Add settlement contract buffers value monitoring test #75

Merged
merged 19 commits into from
Nov 9, 2023
Merged
Show file tree
Hide file tree
Changes from 15 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
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
44 changes: 44 additions & 0 deletions src/apis/klerosapi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"""
KlerosAPI for fetching the Kleros token list.
"""
# pylint: disable=logging-fstring-interpolation

import requests
harisang marked this conversation as resolved.
Show resolved Hide resolved
from src.helper_functions import get_logger
from src.constants import (
header,
REQUEST_TIMEOUT,
)


class KlerosAPI:
"""
Class for fetching the Kleros token list.
"""

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

def get_token_list(self) -> list[str]:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def get_token_list(self) -> list[str]:
def get_token_list(self) -> Optional[list[str]]:

"""
Returns the Kleros token list.
"""
kleros_url = "http://t2crtokens.eth.link"
harisang marked this conversation as resolved.
Show resolved Hide resolved

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
kleros_list: Optional[list[str]] = None

try:
kleros_data = requests.get(
kleros_url,
headers=header,
timeout=REQUEST_TIMEOUT,
)
kleros_rsp = kleros_data.json()
kleros_list: list[str] = []
if "tokens" not in kleros_rsp:
return kleros_list
for token in kleros_rsp["tokens"]:
kleros_list.append(token["address"].lower())
except requests.RequestException as err:
self.logger.warning(
f"Connection error while fetching the Kleros token list, error: {err}"
)
return kleros_list
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without the changes on setting a default, this sometimes crashes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added what is now L36-37, to address the issue.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It still crashes for we when the request times out. Our current convention for that case is to use None as result.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And all these since I cannot catch a general exception here due to the mypy,pylint etc.... Will have another look.

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
98 changes: 98 additions & 0 deletions src/monitoring_tests/buffers_monitoring_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
"""
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.klerosapi import KlerosAPI
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.kleros_api = KlerosAPI()
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()
harisang marked this conversation as resolved.
Show resolved Hide resolved
kleros_list = self.kleros_api.get_token_list()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handle the case of empty kleros_list here somehow. For example

Suggested change
kleros_list = self.kleros_api.get_token_list()
kleros_list = self.kleros_api.get_token_list()
if kleros_list is None:
kleros_list = []

or maybe just skip the hash?


value_in_usd = 0.0
if "tokens" not in ethplorer_rsp:
return False
for token in ethplorer_rsp["tokens"]:
if token["tokenInfo"]["address"] not in kleros_list:
continue
balance = token["balance"]
decimals = int(token["tokenInfo"]["decimals"])
if token["tokenInfo"]["price"] is not False:
fhenneke marked this conversation as resolved.
Show resolved Hide resolved
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:
fhenneke marked this conversation as resolved.
Show resolved Hide resolved
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 150 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:
print("HERE")
harisang marked this conversation as resolved.
Show resolved Hide resolved
success = self.compute_buffers_value()
if success:
self.counter = 0
return True
Loading