-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add settlement contract buffers value monitoring test (#75)
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
Showing
5 changed files
with
203 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |