Skip to content

Commit 033b7ac

Browse files
authored
Merge pull request #107 from felixprotocol/felix-adapter
feat(felix-usde): integrate felix-usde into ethena adapter
2 parents e0af9e3 + 1a9d25f commit 033b7ac

File tree

6 files changed

+283
-0
lines changed

6 files changed

+283
-0
lines changed

abi/felix_usde.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
[
2+
{
3+
"inputs": [
4+
{
5+
"internalType": "uint256",
6+
"name": "shares",
7+
"type": "uint256"
8+
}
9+
],
10+
"name": "convertToAssets",
11+
"outputs": [
12+
{
13+
"internalType": "uint256",
14+
"name": "",
15+
"type": "uint256"
16+
}
17+
],
18+
"stateMutability": "view",
19+
"type": "function"
20+
}
21+
]

constants/felix.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import json
2+
from utils.web3_utils import W3_BY_CHAIN
3+
from constants.chains import Chain
4+
from web3 import Web3
5+
6+
# Load the ERC4626 ABI from the correct path
7+
with open("abi/felix_usde.json") as f:
8+
felix_usde_abi = json.load(f)
9+
10+
# Set the Felix USDe Vault address and contract
11+
FELIX_USDE_VAULT_ADDRESS = Web3.to_checksum_address("0x835FEBF893c6DdDee5CF762B0f8e31C5B06938ab")
12+
FELIX_USDE_VAULT_CONTRACT = W3_BY_CHAIN[Chain.HYPEREVM]["w3"].eth.contract(
13+
address=FELIX_USDE_VAULT_ADDRESS,
14+
abi=felix_usde_abi,
15+
)
16+
17+
# Set the Felix USDe holders subgraph URL
18+
FELIX_USDE_HOLDERS_GRAPH_URL = "https://api.goldsky.com/api/public/project_cmbpnr7pflkhm01vphziu05pr/subgraphs/felix-usde/prod/gn"

constants/summary_columns.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ class SummaryColumn(Enum):
9191
SummaryColumnType.ETHENA_PTS,
9292
)
9393

94+
FELIX_USDE_PTS = ("felix_usde_pts", SummaryColumnType.ETHENA_PTS)
95+
9496
# Terminal Finance
9597
TERMINAL_TUSDE_PTS = (
9698
"terminal_tusde_pts",

integrations/felix_integration.py

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import logging
2+
from typing import Dict, List, Optional, Set
3+
from constants.chains import Chain
4+
from constants.summary_columns import SummaryColumn
5+
from integrations.cached_balances_integration import CachedBalancesIntegration
6+
from integrations.integration_ids import IntegrationID
7+
from web3 import Web3
8+
from eth_typing import ChecksumAddress
9+
from utils.felix import get_users_asset_balances_at_block
10+
11+
12+
class FelixUsdeIntegration(CachedBalancesIntegration):
13+
def __init__(
14+
self,
15+
integration_id: IntegrationID,
16+
start_block: int,
17+
chain: Chain = Chain.HYPEREVM,
18+
summary_cols: Optional[List[SummaryColumn]] = None,
19+
reward_multiplier: int = 1,
20+
excluded_addresses: Optional[Set[ChecksumAddress]] = None,
21+
):
22+
super().__init__(
23+
integration_id=integration_id,
24+
start_block=start_block,
25+
chain=chain,
26+
summary_cols=summary_cols,
27+
reward_multiplier=reward_multiplier,
28+
excluded_addresses=excluded_addresses,
29+
)
30+
31+
32+
def get_block_balances(
33+
self, cached_data: Dict[int, Dict[ChecksumAddress, float]], blocks: List[int]
34+
) -> Dict[int, Dict[ChecksumAddress, float]]:
35+
"""Get user balances for specified blocks, using cached data when available.
36+
37+
Args:
38+
cached_data (Dict[int, Dict[ChecksumAddress, float]]): Dictionary mapping block numbers
39+
to user balances at that block. Used to avoid recomputing known balances.
40+
The inner dictionary maps user addresses to their USDe balance.
41+
blocks (List[int]): List of block numbers to get balances for.
42+
43+
Returns:
44+
Dict[int, Dict[ChecksumAddress, float]]: Dictionary mapping block numbers to user balances,
45+
where each inner dictionary maps user addresses to their USDe balance
46+
at that block.
47+
"""
48+
logging.info("[Felix integration] Getting block balances")
49+
50+
# Initialize result dictionary
51+
result_data: Dict[int, Dict[ChecksumAddress, float]] = {}
52+
53+
# Process each block
54+
for block in blocks:
55+
# Skip blocks before the start block
56+
if block < self.start_block:
57+
result_data[block] = {}
58+
continue
59+
60+
# Use cached data if available
61+
if block in cached_data and cached_data[block]:
62+
logging.info(f"[Felix integration] Using cached data for block {block}")
63+
result_data[block] = cached_data[block]
64+
continue
65+
66+
# Get fresh data for this block
67+
logging.info(f"[Felix integration] Fetching data for block {block}")
68+
try:
69+
block_balances = get_users_asset_balances_at_block(block)
70+
result_data[block] = block_balances
71+
except Exception as e:
72+
logging.error(f"[Felix integration] Error fetching data for block {block}: {e}")
73+
result_data[block] = {}
74+
75+
return result_data
76+
77+
78+
if __name__ == "__main__":
79+
"""
80+
Test script for the Felix USDe integration.
81+
This is for development/testing only and not used when the integration is run as part of the Ethena system.
82+
"""
83+
# Create example integration
84+
example_integration = FelixUsdeIntegration(
85+
integration_id=IntegrationID.FELIX_USDE,
86+
start_block=3450891,
87+
summary_cols=[SummaryColumn.FELIX_USDE_PTS],
88+
chain=Chain.HYPEREVM,
89+
reward_multiplier=1,
90+
excluded_addresses={
91+
Web3.to_checksum_address("0x0000000000000000000000000000000000000000")
92+
},
93+
)
94+
95+
# Test without cached data
96+
print("Testing Felix USDe Integration")
97+
print("=" * 50)
98+
99+
test_blocks = [10200000, 10200001, 10200002]
100+
without_cached_data_output = example_integration.get_block_balances(
101+
cached_data={}, blocks=test_blocks
102+
)
103+
print("Without cached data:")
104+
print(without_cached_data_output)
105+
print()
106+
107+
# Test with cached data
108+
cached_data = {
109+
10200000: {
110+
Web3.to_checksum_address("0x1234567890123456789012345678901234567890"): 100.0,
111+
Web3.to_checksum_address("0x2345678901234567890123456789012345678901"): 200.0,
112+
},
113+
10200001: {
114+
Web3.to_checksum_address("0x1234567890123456789012345678901234567890"): 101.0,
115+
Web3.to_checksum_address("0x2345678901234567890123456789012345678901"): 201.0,
116+
},
117+
}
118+
119+
with_cached_data_output = example_integration.get_block_balances(
120+
cached_data=cached_data, blocks=[10200002]
121+
)
122+
print("With cached data (only fetching block 10200002):")
123+
print(with_cached_data_output)
124+
print()
125+
126+
print("Felix USDe Integration test completed!")

integrations/integration_ids.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -537,6 +537,9 @@ class IntegrationID(Enum):
537537
Token.USDE,
538538
)
539539

540+
# Felix
541+
FELIX_USDE = ("felix_usde", "Felix USDe", Token.USDE)
542+
540543
# Terminal Finance
541544
TERMINAL_TUSDE = (
542545
"terminal_tusde",

utils/felix.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
from utils.request_utils import requests_retry_session
2+
from constants.felix import FELIX_USDE_VAULT_CONTRACT
3+
from typing import Dict
4+
from web3.types import ChecksumAddress
5+
from web3 import Web3
6+
7+
8+
def get_shares_to_assets_ratio_at_block(block_number: int) -> float:
9+
"""
10+
Get the ratio of shares to assets for the Felix USDe vault at a specific block.
11+
"""
12+
shares_base_amount = 100000000000000000000000
13+
try:
14+
# Call convertToAssets with shares_base_amount to get the ratio
15+
result = FELIX_USDE_VAULT_CONTRACT.functions.convertToAssets(shares_base_amount).call(
16+
block_identifier=block_number
17+
)
18+
return result / (shares_base_amount)
19+
except Exception as e:
20+
print(f"Error getting shares to assets ratio: {e}")
21+
return 0.0
22+
23+
24+
def get_users_asset_balances_at_block(block_number: int) -> Dict[ChecksumAddress, float]:
25+
"""
26+
Get users' asset balances at a specific block by:
27+
1. Retrieving active holders' shares from subgraph
28+
2. Applying shares-to-assets ratio to convert shares to assets
29+
3. Returning a dictionary mapping user addresses to their asset balances in USDe
30+
31+
Args:
32+
block_number: Block number to query
33+
batch_size: Number of users to process in each multicall batch (default: 400)
34+
"""
35+
# Get active holders' shares from subgraph
36+
from constants.felix import FELIX_USDE_HOLDERS_GRAPH_URL
37+
active_holders_shares = get_feUSDe_active_holders_balance_at_block(
38+
FELIX_USDE_HOLDERS_GRAPH_URL, block_number
39+
)
40+
if not active_holders_shares:
41+
return {}
42+
43+
# Get the shares-to-assets ratio for this block
44+
shares_to_assets_ratio = get_shares_to_assets_ratio_at_block(block_number)
45+
46+
# Convert shares to assets using the ratio
47+
user_asset_balances = {}
48+
49+
for user_address, shares in active_holders_shares.items():
50+
# Convert string address to ChecksumAddress
51+
checksum_address = Web3.to_checksum_address(user_address)
52+
# Apply ratio to convert shares to assets
53+
asset_balance = shares * shares_to_assets_ratio / (10 ** 18)
54+
user_asset_balances[checksum_address] = asset_balance
55+
56+
return user_asset_balances
57+
58+
59+
def get_feUSDe_active_holders_balance_at_block(graph_url: str, block_number: int):
60+
skip = 0
61+
max_pagination_size = 1000
62+
active_holders_balance = {}
63+
64+
session = requests_retry_session(
65+
retries=5,
66+
backoff_factor=0.5,
67+
status_forcelist=(429, 500, 502, 503, 504)
68+
)
69+
70+
while True:
71+
query = """
72+
{
73+
accounts(first: %s, skip: %s) {
74+
id
75+
snapshots(
76+
where: {blockNumber_lte: %s}
77+
orderBy: blockNumber
78+
orderDirection: desc
79+
first: 1
80+
) {
81+
balance
82+
}
83+
}
84+
}
85+
""" % (max_pagination_size, skip, block_number)
86+
87+
response = session.post(
88+
url=graph_url,
89+
json={'query': query},
90+
timeout=10
91+
)
92+
93+
if response.status_code != 200:
94+
raise Exception(f"Query failed with status code {response.status_code}: {response.text}")
95+
96+
data = response.json()
97+
accounts = data['data']['accounts']
98+
99+
if not accounts:
100+
break
101+
102+
for account in accounts:
103+
# Only include accounts with a positive balance at the block number
104+
if account['snapshots'] and int(account['snapshots'][0]['balance']) > 0:
105+
active_holders_balance[account['id']] = int(account['snapshots'][0]['balance'])
106+
107+
# Stop if we've reached the end of the accounts
108+
if len(accounts) < max_pagination_size:
109+
break
110+
111+
skip += max_pagination_size
112+
113+
return active_holders_balance

0 commit comments

Comments
 (0)