@intentsolutionsio/flash-loan-simulator 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,416 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Flash loan protocol adapters.
4
+
5
+ Abstracts flash loan providers (Aave, dYdX, Balancer) with
6
+ unified interface for fee calculation and liquidity checks.
7
+ """
8
+
9
+ from abc import ABC, abstractmethod
10
+ from dataclasses import dataclass
11
+ from decimal import Decimal
12
+ from typing import Dict, List, Optional
13
+
14
+ import httpx
15
+
16
+
17
+ @dataclass
18
+ class LoanParams:
19
+ """Parameters for a flash loan request."""
20
+
21
+ asset: str
22
+ amount: Decimal
23
+ chain: str = "ethereum"
24
+
25
+
26
+ @dataclass
27
+ class ProviderInfo:
28
+ """Information about a flash loan provider."""
29
+
30
+ name: str
31
+ fee_rate: Decimal
32
+ fee_amount: Decimal
33
+ max_available: Decimal
34
+ supported_chains: List[str]
35
+ gas_overhead: int # Extra gas for using this provider
36
+
37
+
38
+ class FlashLoanProvider(ABC):
39
+ """Abstract base class for flash loan providers."""
40
+
41
+ @property
42
+ @abstractmethod
43
+ def name(self) -> str:
44
+ """Provider name."""
45
+ pass
46
+
47
+ @property
48
+ @abstractmethod
49
+ def fee_rate(self) -> Decimal:
50
+ """Fee rate as decimal (e.g., 0.0009 for 0.09%)."""
51
+ pass
52
+
53
+ @abstractmethod
54
+ def get_fee(self, asset: str, amount: Decimal) -> Decimal:
55
+ """Calculate flash loan fee."""
56
+ pass
57
+
58
+ @abstractmethod
59
+ def get_max_loan(self, asset: str, chain: str = "ethereum") -> Decimal:
60
+ """Get maximum available loan amount."""
61
+ pass
62
+
63
+ @abstractmethod
64
+ def get_supported_assets(self, chain: str = "ethereum") -> List[str]:
65
+ """Get list of supported assets."""
66
+ pass
67
+
68
+ @abstractmethod
69
+ def get_gas_overhead(self) -> int:
70
+ """Get extra gas units for using this provider."""
71
+ pass
72
+
73
+ def get_info(self, asset: str, amount: Decimal, chain: str = "ethereum") -> ProviderInfo:
74
+ """Get complete provider info for a loan."""
75
+ return ProviderInfo(
76
+ name=self.name,
77
+ fee_rate=self.fee_rate,
78
+ fee_amount=self.get_fee(asset, amount),
79
+ max_available=self.get_max_loan(asset, chain),
80
+ supported_chains=self.supported_chains,
81
+ gas_overhead=self.get_gas_overhead(),
82
+ )
83
+
84
+ @property
85
+ @abstractmethod
86
+ def supported_chains(self) -> List[str]:
87
+ """List of supported chains."""
88
+ pass
89
+
90
+
91
+ class AaveV3Provider(FlashLoanProvider):
92
+ """
93
+ Aave V3 flash loan provider.
94
+
95
+ Fee: 0.09% (9 basis points)
96
+ Chains: Ethereum, Polygon, Arbitrum, Optimism, Avalanche
97
+ """
98
+
99
+ FEE_RATE = Decimal("0.0009") # 0.09%
100
+
101
+ POOL_ADDRESSES = {
102
+ "ethereum": "0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2",
103
+ "polygon": "0x794a61358D6845594F94dc1DB02A252b5b4814aD",
104
+ "arbitrum": "0x794a61358D6845594F94dc1DB02A252b5b4814aD",
105
+ "optimism": "0x794a61358D6845594F94dc1DB02A252b5b4814aD",
106
+ "avalanche": "0x794a61358D6845594F94dc1DB02A252b5b4814aD",
107
+ }
108
+
109
+ # Approximate max liquidity per asset (in USD equivalent)
110
+ MAX_LIQUIDITY = {
111
+ "ETH": Decimal("500000"), # ~500K ETH available
112
+ "WETH": Decimal("500000"),
113
+ "USDC": Decimal("1000000000"), # ~1B USDC
114
+ "USDT": Decimal("500000000"),
115
+ "DAI": Decimal("300000000"),
116
+ "WBTC": Decimal("10000"), # ~10K WBTC
117
+ }
118
+
119
+ @property
120
+ def name(self) -> str:
121
+ return "Aave V3"
122
+
123
+ @property
124
+ def fee_rate(self) -> Decimal:
125
+ return self.FEE_RATE
126
+
127
+ @property
128
+ def supported_chains(self) -> List[str]:
129
+ return list(self.POOL_ADDRESSES.keys())
130
+
131
+ def get_fee(self, asset: str, amount: Decimal) -> Decimal:
132
+ """Aave charges flat 0.09% fee."""
133
+ return amount * self.FEE_RATE
134
+
135
+ def get_max_loan(self, asset: str, chain: str = "ethereum") -> Decimal:
136
+ """Get max available from Aave pools."""
137
+ asset_upper = asset.upper()
138
+ return self.MAX_LIQUIDITY.get(asset_upper, Decimal("0"))
139
+
140
+ def get_supported_assets(self, chain: str = "ethereum") -> List[str]:
141
+ """Aave supports most major tokens."""
142
+ return list(self.MAX_LIQUIDITY.keys())
143
+
144
+ def get_gas_overhead(self) -> int:
145
+ """Aave flash loans add ~100k gas overhead."""
146
+ return 100000
147
+
148
+
149
+ class DydxProvider(FlashLoanProvider):
150
+ """
151
+ dYdX flash loan provider.
152
+
153
+ Fee: 0% (FREE flash loans!)
154
+ Chains: Ethereum only
155
+ Assets: ETH, USDC, DAI, WBTC (limited selection)
156
+ """
157
+
158
+ FEE_RATE = Decimal("0") # 0% - FREE!
159
+
160
+ SOLO_MARGIN_ADDRESS = "0x1E0447b19BB6EcFdAe1e4AE1694b0C3659614e4e"
161
+
162
+ SUPPORTED_ASSETS = ["ETH", "WETH", "USDC", "DAI", "WBTC"]
163
+
164
+ MAX_LIQUIDITY = {
165
+ "ETH": Decimal("50000"),
166
+ "WETH": Decimal("50000"),
167
+ "USDC": Decimal("100000000"),
168
+ "DAI": Decimal("50000000"),
169
+ "WBTC": Decimal("1000"),
170
+ }
171
+
172
+ @property
173
+ def name(self) -> str:
174
+ return "dYdX"
175
+
176
+ @property
177
+ def fee_rate(self) -> Decimal:
178
+ return self.FEE_RATE
179
+
180
+ @property
181
+ def supported_chains(self) -> List[str]:
182
+ return ["ethereum"]
183
+
184
+ def get_fee(self, asset: str, amount: Decimal) -> Decimal:
185
+ """dYdX has 0% flash loan fee!"""
186
+ return Decimal("0")
187
+
188
+ def get_max_loan(self, asset: str, chain: str = "ethereum") -> Decimal:
189
+ """Get max available from dYdX."""
190
+ if chain != "ethereum":
191
+ return Decimal("0")
192
+ return self.MAX_LIQUIDITY.get(asset.upper(), Decimal("0"))
193
+
194
+ def get_supported_assets(self, chain: str = "ethereum") -> List[str]:
195
+ """dYdX has limited asset support."""
196
+ if chain != "ethereum":
197
+ return []
198
+ return self.SUPPORTED_ASSETS
199
+
200
+ def get_gas_overhead(self) -> int:
201
+ """dYdX flash loans are slightly more gas-intensive."""
202
+ return 150000
203
+
204
+
205
+ class BalancerProvider(FlashLoanProvider):
206
+ """
207
+ Balancer flash loan provider.
208
+
209
+ Fee: 0.01% (1 basis point) - Very cheap!
210
+ Chains: Ethereum, Polygon, Arbitrum
211
+ Assets: Any pool token
212
+ """
213
+
214
+ FEE_RATE = Decimal("0.0001") # 0.01%
215
+
216
+ VAULT_ADDRESSES = {
217
+ "ethereum": "0xBA12222222228d8Ba445958a75a0704d566BF2C8",
218
+ "polygon": "0xBA12222222228d8Ba445958a75a0704d566BF2C8",
219
+ "arbitrum": "0xBA12222222228d8Ba445958a75a0704d566BF2C8",
220
+ }
221
+
222
+ MAX_LIQUIDITY = {
223
+ "ETH": Decimal("100000"),
224
+ "WETH": Decimal("100000"),
225
+ "USDC": Decimal("200000000"),
226
+ "DAI": Decimal("100000000"),
227
+ "WBTC": Decimal("5000"),
228
+ "BAL": Decimal("10000000"),
229
+ }
230
+
231
+ @property
232
+ def name(self) -> str:
233
+ return "Balancer"
234
+
235
+ @property
236
+ def fee_rate(self) -> Decimal:
237
+ return self.FEE_RATE
238
+
239
+ @property
240
+ def supported_chains(self) -> List[str]:
241
+ return list(self.VAULT_ADDRESSES.keys())
242
+
243
+ def get_fee(self, asset: str, amount: Decimal) -> Decimal:
244
+ """Balancer charges 0.01% fee."""
245
+ return amount * self.FEE_RATE
246
+
247
+ def get_max_loan(self, asset: str, chain: str = "ethereum") -> Decimal:
248
+ """Get max available from Balancer pools."""
249
+ if chain not in self.VAULT_ADDRESSES:
250
+ return Decimal("0")
251
+ return self.MAX_LIQUIDITY.get(asset.upper(), Decimal("0"))
252
+
253
+ def get_supported_assets(self, chain: str = "ethereum") -> List[str]:
254
+ """Balancer supports pool tokens."""
255
+ return list(self.MAX_LIQUIDITY.keys())
256
+
257
+ def get_gas_overhead(self) -> int:
258
+ """Balancer flash loans are gas-efficient."""
259
+ return 80000
260
+
261
+
262
+ class UniswapV3Provider(FlashLoanProvider):
263
+ """
264
+ Uniswap V3 flash swap provider.
265
+
266
+ Fee: Pool fee (~0.3% for most pairs, but paid implicitly)
267
+ Note: Technically a flash swap, not a loan - you receive
268
+ tokens upfront and pay back with fee in same tx.
269
+ """
270
+
271
+ # Pool fees vary: 0.01%, 0.05%, 0.3%, 1%
272
+ # We use 0.3% as default (most common)
273
+ FEE_RATE = Decimal("0.003")
274
+
275
+ ROUTER_ADDRESSES = {
276
+ "ethereum": "0xE592427A0AEce92De3Edee1F18E0157C05861564",
277
+ "polygon": "0xE592427A0AEce92De3Edee1F18E0157C05861564",
278
+ "arbitrum": "0xE592427A0AEce92De3Edee1F18E0157C05861564",
279
+ "optimism": "0xE592427A0AEce92De3Edee1F18E0157C05861564",
280
+ }
281
+
282
+ MAX_LIQUIDITY = {
283
+ "ETH": Decimal("200000"),
284
+ "WETH": Decimal("200000"),
285
+ "USDC": Decimal("500000000"),
286
+ "USDT": Decimal("300000000"),
287
+ "DAI": Decimal("200000000"),
288
+ "WBTC": Decimal("5000"),
289
+ }
290
+
291
+ @property
292
+ def name(self) -> str:
293
+ return "Uniswap V3"
294
+
295
+ @property
296
+ def fee_rate(self) -> Decimal:
297
+ return self.FEE_RATE
298
+
299
+ @property
300
+ def supported_chains(self) -> List[str]:
301
+ return list(self.ROUTER_ADDRESSES.keys())
302
+
303
+ def get_fee(self, asset: str, amount: Decimal) -> Decimal:
304
+ """Uniswap charges pool fee (implicit in swap)."""
305
+ return amount * self.FEE_RATE
306
+
307
+ def get_max_loan(self, asset: str, chain: str = "ethereum") -> Decimal:
308
+ """Get max available from Uniswap pools."""
309
+ if chain not in self.ROUTER_ADDRESSES:
310
+ return Decimal("0")
311
+ return self.MAX_LIQUIDITY.get(asset.upper(), Decimal("0"))
312
+
313
+ def get_supported_assets(self, chain: str = "ethereum") -> List[str]:
314
+ """Uniswap supports any paired token."""
315
+ return list(self.MAX_LIQUIDITY.keys())
316
+
317
+ def get_gas_overhead(self) -> int:
318
+ """Uniswap flash swaps are moderately gas-intensive."""
319
+ return 120000
320
+
321
+
322
+ class ProviderManager:
323
+ """
324
+ Manages all flash loan providers.
325
+
326
+ Provides comparison and selection capabilities.
327
+ """
328
+
329
+ def __init__(self):
330
+ """Initialize with all providers."""
331
+ self.providers: Dict[str, FlashLoanProvider] = {
332
+ "aave": AaveV3Provider(),
333
+ "dydx": DydxProvider(),
334
+ "balancer": BalancerProvider(),
335
+ "uniswap": UniswapV3Provider(),
336
+ }
337
+
338
+ def get_provider(self, name: str) -> Optional[FlashLoanProvider]:
339
+ """Get provider by name."""
340
+ return self.providers.get(name.lower())
341
+
342
+ def list_providers(self) -> List[str]:
343
+ """List all available providers."""
344
+ return list(self.providers.keys())
345
+
346
+ def compare_providers(
347
+ self, asset: str, amount: Decimal, chain: str = "ethereum"
348
+ ) -> List[ProviderInfo]:
349
+ """
350
+ Compare all providers for a specific loan.
351
+
352
+ Returns providers sorted by total cost (lowest first).
353
+ """
354
+ results = []
355
+
356
+ for provider in self.providers.values():
357
+ if chain not in provider.supported_chains:
358
+ continue
359
+ if asset.upper() not in provider.get_supported_assets(chain):
360
+ continue
361
+ if amount > provider.get_max_loan(asset, chain):
362
+ continue
363
+
364
+ info = provider.get_info(asset, amount, chain)
365
+ results.append(info)
366
+
367
+ # Sort by fee amount (cheapest first)
368
+ results.sort(key=lambda x: x.fee_amount)
369
+ return results
370
+
371
+ def find_cheapest(
372
+ self, asset: str, amount: Decimal, chain: str = "ethereum"
373
+ ) -> Optional[ProviderInfo]:
374
+ """Find the cheapest provider for a loan."""
375
+ providers = self.compare_providers(asset, amount, chain)
376
+ return providers[0] if providers else None
377
+
378
+
379
+ def demo():
380
+ """Demonstrate provider comparison."""
381
+ manager = ProviderManager()
382
+
383
+ print("=" * 60)
384
+ print("FLASH LOAN PROVIDER COMPARISON")
385
+ print("=" * 60)
386
+
387
+ # Compare for 100 ETH loan
388
+ asset = "ETH"
389
+ amount = Decimal("100")
390
+
391
+ print(f"\nComparing providers for {amount} {asset} flash loan:")
392
+ print("-" * 60)
393
+
394
+ providers = manager.compare_providers(asset, amount)
395
+
396
+ for info in providers:
397
+ fee_pct = float(info.fee_rate) * 100
398
+ print(f"\n{info.name}:")
399
+ print(f" Fee Rate: {fee_pct:.2f}%")
400
+ print(f" Fee Amount: {info.fee_amount:.4f} {asset}")
401
+ print(f" Max Available: {info.max_available:,.0f} {asset}")
402
+ print(f" Gas Overhead: {info.gas_overhead:,} units")
403
+ print(f" Chains: {', '.join(info.supported_chains)}")
404
+
405
+ # Find cheapest
406
+ cheapest = manager.find_cheapest(asset, amount)
407
+ if cheapest:
408
+ print(f"\n{'=' * 60}")
409
+ print(f"RECOMMENDATION: {cheapest.name}")
410
+ print(f" Lowest fee: {cheapest.fee_amount:.4f} {asset}")
411
+ if cheapest.fee_amount == 0:
412
+ print(" (FREE flash loan!)")
413
+
414
+
415
+ if __name__ == "__main__":
416
+ demo()