Skip to content

Commit 444d95b

Browse files
authored
Merge pull request #574 from waterscape03/feat/vesu_research
vesu research
2 parents 930208b + 300fdc1 commit 444d95b

File tree

2 files changed

+360
-0
lines changed

2 files changed

+360
-0
lines changed

apps/data_handler/handlers/loan_states/vesu/__init__.py

Whitespace-only changes.
Lines changed: 360 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,360 @@
1+
"""
2+
Module to handle VesuLoan events and calculate health factors.
3+
This module interacts with the VesuLoan contract on Starknet to fetch user positions,
4+
calculate collateral and debt values, and determine health factors for users.
5+
"""
6+
7+
from decimal import Decimal
8+
9+
from shared.starknet_client import StarknetClient
10+
from starknet_py.hash.selector import get_selector_from_name
11+
12+
13+
class VesuLoanEntity:
14+
"""
15+
Class to handle VesuLoan events and calculate health factors.
16+
This class interacts with the VesuLoan contract on Starknet to fetch user positions,
17+
calculate collateral and debt values, and determine health factors for users.
18+
It uses a mock database to store user positions and a cache to optimize data retrieval.
19+
"""
20+
21+
VESU_ADDRESS = "0x02545b2e5d519fc230e9cd781046d3a64e092114f07e44771e0d719d148725ef"
22+
23+
def __init__(self):
24+
"""Initialize Starknet client and storage."""
25+
self.client = StarknetClient()
26+
self.mock_db = {}
27+
self._cache = {}
28+
self.last_processed_block = 654244 # First VESU event block
29+
30+
async def _get_token_decimals(self, token_address: int) -> Decimal:
31+
"""
32+
Fetch decimals directly from the token contract
33+
34+
:param token_address: Token address in decimal format
35+
36+
:return: Decimal factor based on token decimals (10^n)
37+
"""
38+
result = await self.client.func_call(token_address, "decimals", [])
39+
if result and len(result) > 0:
40+
print(f"Decimals for token {hex(token_address)}: {result[0]}")
41+
decimals = result[0]
42+
return Decimal(10) ** Decimal(decimals)
43+
return Decimal("Inf")
44+
45+
async def calculate_health_factor(self, user_address: int) -> dict:
46+
"""
47+
Calculate health factors for all positions of a user.
48+
49+
:param user_address: User address in int format
50+
51+
:return: Dictionary with pool IDs as keys and health factors as values
52+
"""
53+
54+
user_positions = {k: v for k, v in self.mock_db.items() if k[0] == user_address}
55+
56+
if not user_positions:
57+
return {}
58+
59+
results = {}
60+
for (_, pool_id), position_data in user_positions.items():
61+
collateral_asset = position_data["collateral_asset"]
62+
debt_asset = position_data["debt_asset"]
63+
64+
if collateral_asset == 0 or debt_asset == 0:
65+
results[hex(pool_id)] = Decimal("inf")
66+
continue
67+
68+
position = await self._get_position_data(
69+
user_address, pool_id, collateral_asset, debt_asset
70+
)
71+
collateral_shares_low, collateral_shares_high = position[0], position[1]
72+
collateral_sign = 0 if collateral_shares_low >= 0 else 1
73+
74+
collateral_value = await self._get_collateral_value(
75+
pool_id,
76+
collateral_asset,
77+
collateral_shares_low,
78+
collateral_shares_high,
79+
collateral_sign,
80+
)
81+
82+
debt_config = await self._get_asset_config(pool_id, debt_asset)
83+
nominal_debt_low, nominal_debt_high = position[2], position[3]
84+
debt_sign = 0 if nominal_debt_low >= 0 else 1
85+
86+
rate_acc_low, rate_acc_high = debt_config[14], debt_config[15]
87+
scale_low, scale_high = debt_config[10], debt_config[11]
88+
89+
debt_value = await self._calculate_debt(
90+
nominal_debt_low,
91+
nominal_debt_high,
92+
debt_sign,
93+
rate_acc_low,
94+
rate_acc_high,
95+
scale_low,
96+
scale_high,
97+
)
98+
99+
ltv_data = await self.get_ltv_config(pool_id, collateral_asset, debt_asset)
100+
101+
collateral_decimals = await self._get_token_decimals(collateral_asset)
102+
debt_decimals = await self._get_token_decimals(debt_asset)
103+
104+
collateral_factor = Decimal(ltv_data[0]) / collateral_decimals
105+
106+
collateral_price = await self.fetch_token_price(collateral_asset, pool_id)
107+
debt_price = await self.fetch_token_price(debt_asset, pool_id)
108+
109+
collateral_normalized = collateral_value / collateral_decimals
110+
debt_normalized = debt_value / debt_decimals
111+
112+
collateral_usd = collateral_normalized * collateral_price
113+
debt_usd = debt_normalized * debt_price
114+
115+
health_factor = (
116+
(collateral_usd * collateral_factor) / debt_usd
117+
if debt_usd > 0
118+
else Decimal("inf")
119+
)
120+
121+
results[hex(pool_id)] = health_factor
122+
123+
return results
124+
125+
async def _get_position_data(
126+
self, user, pool_id, collateral_asset, debt_asset
127+
) -> tuple:
128+
"""
129+
Get user position data with caching.
130+
:param user: User address in decimal format
131+
:param pool_id: Pool ID in decimal
132+
:param collateral_asset: Collateral asset address in decimal format
133+
:param debt_asset: Debt asset address in decimal format
134+
:return: Position data as a tuple
135+
"""
136+
vesu_addr = int(self.VESU_ADDRESS, 16)
137+
138+
cache_key = f"position_{user}_{pool_id}_{collateral_asset}_{debt_asset}"
139+
return await self._get_contract_data(
140+
cache_key,
141+
self.client.func_call,
142+
[vesu_addr, "position", [pool_id, collateral_asset, debt_asset, user]],
143+
)
144+
145+
async def _get_contract_data(self, cache_key, func, params) -> tuple:
146+
"""
147+
Get data from contract with caching.
148+
149+
:param cache_key: Cache key for the data
150+
:param func: Function to call
151+
:param params: Parameters for the function call
152+
:return: Result of the function call
153+
"""
154+
if cache_key not in self._cache:
155+
self._cache[cache_key] = await func(*params)
156+
return self._cache[cache_key]
157+
158+
async def _get_collateral_value(
159+
self, pool_id, asset, shares_low, shares_high, sign=0
160+
) -> Decimal:
161+
"""
162+
Calculate collateral value.
163+
164+
:param pool_id: Pool ID in decimal
165+
:param asset: Asset address in decimal format
166+
:param shares_low: Low part of shares
167+
:param shares_high: High part of shares
168+
:param sign: Sign of the shares (0 or 1)
169+
:return: Collateral value as a Decimal
170+
"""
171+
vesu_addr = int(self.VESU_ADDRESS, 16)
172+
173+
cache_key = f"collateral_{asset}_{shares_low}_{shares_high}_{sign}"
174+
result = await self._get_contract_data(
175+
cache_key,
176+
self.client.func_call,
177+
[
178+
vesu_addr,
179+
"calculate_collateral",
180+
[pool_id, asset, shares_low, shares_high, sign],
181+
],
182+
)
183+
return self._u256_to_decimal(result[0], result[1])
184+
185+
async def _calculate_debt(
186+
self,
187+
nominal_debt_low,
188+
nominal_debt_high,
189+
sign,
190+
rate_accumulator_low,
191+
rate_accumulator_high,
192+
scale_low,
193+
scale_high,
194+
) -> Decimal:
195+
"""
196+
Calculate debt value.
197+
198+
:param nominal_debt_low: Low part of nominal debt
199+
:param nominal_debt_high: High part of nominal debt
200+
:param sign: Sign of the debt (0 or 1)
201+
:param rate_accumulator_low: Low part of rate accumulator
202+
:param rate_accumulator_high: High part of rate accumulator
203+
:param scale_low: Low part of scale
204+
:param scale_high: High part of scale
205+
:return: Debt value as a Decimal
206+
"""
207+
vesu_addr = int(self.VESU_ADDRESS, 16)
208+
209+
cache_key = f"debt_{nominal_debt_low}_{nominal_debt_high}_{sign}_ \
210+
{rate_accumulator_low}_{rate_accumulator_high}"
211+
result = await self._get_contract_data(
212+
cache_key,
213+
self.client.func_call,
214+
[
215+
vesu_addr,
216+
"calculate_debt",
217+
[
218+
nominal_debt_low,
219+
nominal_debt_high,
220+
sign,
221+
rate_accumulator_low,
222+
rate_accumulator_high,
223+
scale_low,
224+
scale_high,
225+
],
226+
],
227+
)
228+
return self._u256_to_decimal(result[0], result[1])
229+
230+
async def _get_asset_config(self, pool_id, asset_address) -> tuple:
231+
"""
232+
Get asset configuration with caching.
233+
234+
:param pool_id: Pool ID in decimal
235+
:param asset_address: Asset address in decimal format
236+
:return: Asset configuration as a tuple
237+
"""
238+
vesu_addr = int(self.VESU_ADDRESS, 16)
239+
240+
cache_key = f"asset_config_{pool_id}_{asset_address}"
241+
return await self._get_contract_data(
242+
cache_key,
243+
self.client.func_call,
244+
[vesu_addr, "asset_config", [pool_id, asset_address]],
245+
)
246+
247+
async def get_ltv_config(self, pool_id, collateral, debt) -> tuple:
248+
"""
249+
Get LTV configuration with caching.
250+
251+
:param pool_id: Pool ID in decimal
252+
:param collateral: Collateral asset address in decimal format
253+
:param debt: Debt asset address in decimal format
254+
:return: LTV configuration as a tuple
255+
"""
256+
vesu_addr = int(self.VESU_ADDRESS, 16)
257+
258+
cache_key = f"ltv_{pool_id}_{collateral}_{debt}"
259+
return await self._get_contract_data(
260+
cache_key,
261+
self.client.func_call,
262+
[vesu_addr, "ltv_config", [pool_id, collateral, debt]],
263+
)
264+
265+
async def fetch_token_price(self, address, pool_id) -> Decimal:
266+
"""
267+
Fetch token price using price extension.
268+
269+
:param address: Token address in decimal format
270+
:param pool_id: Pool ID in decimal
271+
:return: Token price as a Decimal
272+
"""
273+
try:
274+
vesu_addr = int(self.VESU_ADDRESS, 16)
275+
276+
cache_key = f"extension_price_{pool_id}"
277+
if cache_key not in self._cache:
278+
extension_result = await self.client.func_call(
279+
vesu_addr, "extension", [pool_id]
280+
)
281+
self._cache[cache_key] = extension_result[0]
282+
price_extension_addr = self._cache[cache_key]
283+
284+
price_result = await self.client.func_call(
285+
price_extension_addr, "price", [pool_id, address]
286+
)
287+
price_u256 = price_result[0]
288+
is_valid = price_result[2] if len(price_result) > 2 else 1
289+
290+
if not is_valid:
291+
print(
292+
f"Warning: Price for \
293+
{address} is not valid"
294+
)
295+
return Decimal("1")
296+
297+
return Decimal(price_u256) / Decimal(10**18)
298+
299+
except Exception as e:
300+
print(f"Error fetching price for {address}: {e}")
301+
return Decimal("1")
302+
303+
def _u256_to_decimal(self, low, high) -> Decimal:
304+
"""
305+
Convert u256 (low, high) pair to Decimal.
306+
307+
:param low: Low part of u256
308+
:param high: High part of u256
309+
310+
:return: Decimal representation of u256
311+
"""
312+
return Decimal(low) + (Decimal(high) * Decimal(2**128))
313+
314+
async def update_positions_data(self) -> None:
315+
"""
316+
Process ModifyPosition events to track user positions.
317+
Uses continuation tokens to fetch all events across multiple pages.
318+
319+
:return: None
320+
"""
321+
current_block = await self.client.client.get_block_number()
322+
323+
if current_block <= self.last_processed_block:
324+
return
325+
326+
continuation_token = None
327+
all_events = []
328+
329+
while True:
330+
events = await self.client.client.get_events(
331+
address=self.VESU_ADDRESS,
332+
from_block_number=self.last_processed_block + 1,
333+
to_block_number=current_block,
334+
keys=[[hex(get_selector_from_name("ModifyPosition"))]],
335+
chunk_size=5,
336+
continuation_token=continuation_token,
337+
)
338+
339+
all_events.extend(events.events)
340+
341+
continuation_token = events.continuation_token
342+
if not continuation_token:
343+
break
344+
345+
for event in all_events:
346+
event_keys = event.keys
347+
348+
pool_id = event_keys[1]
349+
collateral_asset = event_keys[2]
350+
debt_asset = event_keys[3]
351+
user = event_keys[4]
352+
block_number = event.block_number
353+
354+
self.mock_db[(user, pool_id)] = {
355+
"pool_id": pool_id,
356+
"collateral_asset": collateral_asset,
357+
"debt_asset": debt_asset,
358+
"block_number": block_number,
359+
}
360+
self.last_processed_block = max(self.last_processed_block, block_number)

0 commit comments

Comments
 (0)