@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.
- package/.claude-plugin/plugin.json +22 -0
- package/LICENSE +21 -0
- package/README.md +411 -0
- package/agents/flashloan-agent.md +261 -0
- package/package.json +43 -0
- package/skills/simulating-flash-loans/ARD.md +454 -0
- package/skills/simulating-flash-loans/PRD.md +239 -0
- package/skills/simulating-flash-loans/SKILL.md +109 -0
- package/skills/simulating-flash-loans/config/settings.yaml +241 -0
- package/skills/simulating-flash-loans/references/errors.md +297 -0
- package/skills/simulating-flash-loans/references/examples.md +532 -0
- package/skills/simulating-flash-loans/references/implementation.md +73 -0
- package/skills/simulating-flash-loans/scripts/flash_simulator.py +492 -0
- package/skills/simulating-flash-loans/scripts/formatters.py +512 -0
- package/skills/simulating-flash-loans/scripts/profit_calculator.py +315 -0
- package/skills/simulating-flash-loans/scripts/protocol_adapters.py +416 -0
- package/skills/simulating-flash-loans/scripts/risk_assessor.py +439 -0
- package/skills/simulating-flash-loans/scripts/strategy_engine.py +596 -0
|
@@ -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()
|