Skip to content

Commit

Permalink
Add settlement contract buffers value monitoring test (#75)
Browse files Browse the repository at this point in the history
This PR adds a test that monitors the value of buffers by using price
feeds from ethplorer and coingecko, while restricting the tokens that
are checked to the ones included in one of the following lists
(whichever can be recovered at the time of the check):

- "http://t2crtokens.eth.link"
- "https://tokens.1inch.eth.link"
- "https://tokenlist.aave.eth.link"

It checks the value of buffers every `BUFFER_INTERVAL` settlements, a
constant currently set to `150`, and if the value is above
`BUFFER_VALUE_THRESHOLD` USD, a constant currently set to `200,000`, it
generates an alert.

The purpose of this PR is to get a better understanding of how quickly
our buffers' value increases, since this might be relevant if we want to
reduce the bonding pool size requirements.

---------

Co-authored-by: Felix Henneke <felix.henneke@protonmail.com>
  • Loading branch information
harisang and fhenneke authored Nov 9, 2023
1 parent 5c36f61 commit 831ec31
Show file tree
Hide file tree
Showing 5 changed files with 203 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
49 changes: 49 additions & 0 deletions src/apis/tokenlistapi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"""
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
99 changes: 99 additions & 0 deletions src/monitoring_tests/buffers_monitoring_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
"""
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 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:
success = self.compute_buffers_value()
if success:
self.counter = 0
return True

0 comments on commit 831ec31

Please sign in to comment.