wayfinder-paths 0.1.8__py3-none-any.whl → 0.1.10__py3-none-any.whl
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.
Potentially problematic release.
This version of wayfinder-paths might be problematic. Click here for more details.
- wayfinder_paths/CONFIG_GUIDE.md +6 -15
- wayfinder_paths/adapters/balance_adapter/README.md +1 -2
- wayfinder_paths/adapters/balance_adapter/adapter.py +4 -4
- wayfinder_paths/adapters/brap_adapter/README.md +1 -1
- wayfinder_paths/adapters/brap_adapter/adapter.py +139 -74
- wayfinder_paths/adapters/hyperlend_adapter/adapter.py +0 -7
- wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +0 -54
- wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py +1 -1
- wayfinder_paths/adapters/ledger_adapter/README.md +1 -1
- wayfinder_paths/adapters/moonwell_adapter/README.md +174 -0
- wayfinder_paths/adapters/moonwell_adapter/__init__.py +7 -0
- wayfinder_paths/adapters/moonwell_adapter/adapter.py +1226 -0
- wayfinder_paths/adapters/moonwell_adapter/test_adapter.py +635 -0
- wayfinder_paths/adapters/pool_adapter/README.md +1 -77
- wayfinder_paths/adapters/pool_adapter/adapter.py +0 -122
- wayfinder_paths/adapters/pool_adapter/examples.json +0 -57
- wayfinder_paths/adapters/pool_adapter/test_adapter.py +0 -86
- wayfinder_paths/adapters/token_adapter/README.md +1 -1
- wayfinder_paths/core/clients/ClientManager.py +1 -22
- wayfinder_paths/core/clients/WalletClient.py +0 -8
- wayfinder_paths/core/clients/WayfinderClient.py +7 -12
- wayfinder_paths/core/clients/__init__.py +0 -8
- wayfinder_paths/core/clients/protocols.py +0 -60
- wayfinder_paths/core/config.py +5 -45
- wayfinder_paths/core/constants/__init__.py +0 -2
- wayfinder_paths/core/constants/base.py +6 -2
- wayfinder_paths/core/constants/moonwell_abi.py +411 -0
- wayfinder_paths/core/services/base.py +7 -1
- wayfinder_paths/core/services/local_evm_txn.py +223 -222
- wayfinder_paths/core/services/local_token_txn.py +103 -92
- wayfinder_paths/core/services/web3_service.py +0 -2
- wayfinder_paths/core/settings.py +8 -8
- wayfinder_paths/core/strategies/Strategy.py +1 -5
- wayfinder_paths/core/strategies/descriptors.py +1 -1
- wayfinder_paths/core/utils/evm_helpers.py +7 -12
- wayfinder_paths/core/wallets/README.md +3 -6
- wayfinder_paths/run_strategy.py +62 -105
- wayfinder_paths/scripts/create_strategy.py +2 -27
- wayfinder_paths/scripts/make_wallets.py +1 -25
- wayfinder_paths/scripts/run_strategy.py +37 -9
- wayfinder_paths/strategies/basis_trading_strategy/snapshot_mixin.py +1 -3
- wayfinder_paths/strategies/basis_trading_strategy/strategy.py +87 -138
- wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +96 -58
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/README.md +2 -17
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/examples.json +4 -1
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +107 -29
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +53 -14
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/README.md +108 -0
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/examples.json +11 -0
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +2975 -0
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/test_strategy.py +886 -0
- wayfinder_paths/strategies/stablecoin_yield_strategy/README.md +0 -7
- wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +2 -7
- wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +0 -4
- wayfinder_paths/templates/adapter/README.md +5 -21
- wayfinder_paths/templates/adapter/adapter.py +1 -2
- wayfinder_paths/templates/adapter/test_adapter.py +1 -1
- wayfinder_paths/templates/strategy/README.md +4 -21
- wayfinder_paths/templates/strategy/test_strategy.py +0 -4
- wayfinder_paths/tests/test_smoke_manifest.py +17 -2
- {wayfinder_paths-0.1.8.dist-info → wayfinder_paths-0.1.10.dist-info}/METADATA +64 -201
- {wayfinder_paths-0.1.8.dist-info → wayfinder_paths-0.1.10.dist-info}/RECORD +64 -71
- wayfinder_paths/adapters/balance_adapter/manifest.yaml +0 -8
- wayfinder_paths/adapters/brap_adapter/manifest.yaml +0 -11
- wayfinder_paths/adapters/hyperlend_adapter/manifest.yaml +0 -10
- wayfinder_paths/adapters/hyperliquid_adapter/manifest.yaml +0 -8
- wayfinder_paths/adapters/ledger_adapter/manifest.yaml +0 -11
- wayfinder_paths/adapters/pool_adapter/manifest.yaml +0 -10
- wayfinder_paths/adapters/token_adapter/manifest.yaml +0 -6
- wayfinder_paths/core/clients/SimulationClient.py +0 -192
- wayfinder_paths/core/clients/TransactionClient.py +0 -63
- wayfinder_paths/core/engine/manifest.py +0 -97
- wayfinder_paths/scripts/validate_manifests.py +0 -213
- wayfinder_paths/strategies/basis_trading_strategy/manifest.yaml +0 -23
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/manifest.yaml +0 -7
- wayfinder_paths/strategies/stablecoin_yield_strategy/manifest.yaml +0 -17
- wayfinder_paths/templates/adapter/manifest.yaml +0 -6
- wayfinder_paths/templates/strategy/manifest.yaml +0 -8
- {wayfinder_paths-0.1.8.dist-info → wayfinder_paths-0.1.10.dist-info}/LICENSE +0 -0
- {wayfinder_paths-0.1.8.dist-info → wayfinder_paths-0.1.10.dist-info}/WHEEL +0 -0
|
@@ -112,7 +112,7 @@ class HyperlendStableYieldStrategy(Strategy):
|
|
|
112
112
|
gas_threshold=MAX_GAS / 3,
|
|
113
113
|
# risk indicators
|
|
114
114
|
volatility=Volatility.LOW,
|
|
115
|
-
|
|
115
|
+
volatility_description=(
|
|
116
116
|
"Pure HyperLend stablecoin lending keeps NAV steady aside from rate drift."
|
|
117
117
|
),
|
|
118
118
|
directionality=Directionality.MARKET_NEUTRAL,
|
|
@@ -195,7 +195,6 @@ class HyperlendStableYieldStrategy(Strategy):
|
|
|
195
195
|
*,
|
|
196
196
|
main_wallet: dict[str, Any] | None = None,
|
|
197
197
|
strategy_wallet: dict[str, Any] | None = None,
|
|
198
|
-
simulation: bool = False,
|
|
199
198
|
web3_service: Web3Service = None,
|
|
200
199
|
api_key: str | None = None,
|
|
201
200
|
):
|
|
@@ -207,7 +206,6 @@ class HyperlendStableYieldStrategy(Strategy):
|
|
|
207
206
|
merged_config["strategy_wallet"] = strategy_wallet
|
|
208
207
|
|
|
209
208
|
self.config = merged_config
|
|
210
|
-
self.simulation = simulation
|
|
211
209
|
self.balance_adapter = None
|
|
212
210
|
self.tx_adapter = None
|
|
213
211
|
self.token_adapter = None
|
|
@@ -238,7 +236,6 @@ class HyperlendStableYieldStrategy(Strategy):
|
|
|
238
236
|
token_transaction_service = LocalTokenTxnService(
|
|
239
237
|
adapter_config,
|
|
240
238
|
wallet_provider=wallet_provider,
|
|
241
|
-
simulation=self.simulation,
|
|
242
239
|
)
|
|
243
240
|
web3_service = DefaultWeb3Service(
|
|
244
241
|
wallet_provider=wallet_provider,
|
|
@@ -250,12 +247,9 @@ class HyperlendStableYieldStrategy(Strategy):
|
|
|
250
247
|
balance = BalanceAdapter(adapter_config, web3_service=web3_service)
|
|
251
248
|
token_adapter = TokenAdapter()
|
|
252
249
|
ledger_adapter = LedgerAdapter() # here
|
|
253
|
-
brap_adapter = BRAPAdapter(
|
|
254
|
-
web3_service=web3_service, simulation=self.simulation
|
|
255
|
-
)
|
|
250
|
+
brap_adapter = BRAPAdapter(web3_service=web3_service)
|
|
256
251
|
hyperlend_adapter = HyperlendAdapter(
|
|
257
252
|
adapter_config,
|
|
258
|
-
simulation=self.simulation,
|
|
259
253
|
web3_service=web3_service,
|
|
260
254
|
)
|
|
261
255
|
|
|
@@ -323,11 +317,109 @@ class HyperlendStableYieldStrategy(Strategy):
|
|
|
323
317
|
self.hys_z: float = self.HYSTERESIS_Z
|
|
324
318
|
self.rotation_tx_cost: float = self.ROTATION_TX_COST
|
|
325
319
|
|
|
326
|
-
async def deposit(
|
|
320
|
+
async def deposit(
|
|
321
|
+
self, main_token_amount: float = 0.0, gas_token_amount: float = 0.0
|
|
322
|
+
) -> StatusTuple:
|
|
323
|
+
if main_token_amount == 0.0 and gas_token_amount == 0.0:
|
|
324
|
+
return (
|
|
325
|
+
False,
|
|
326
|
+
"Either main_token_amount or gas_token_amount must be provided",
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
if main_token_amount > 0:
|
|
330
|
+
if main_token_amount < self.MIN_USDT0_DEPOSIT_AMOUNT:
|
|
331
|
+
return (
|
|
332
|
+
False,
|
|
333
|
+
f"Main token amount {main_token_amount} is below minimum {self.MIN_USDT0_DEPOSIT_AMOUNT}",
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
if gas_token_amount and gas_token_amount > self.GAS_MAXIMUM:
|
|
337
|
+
return (
|
|
338
|
+
False,
|
|
339
|
+
f"Gas token amount exceeds maximum configured gas buffer: {self.GAS_MAXIMUM}",
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
if self.balance_adapter is None:
|
|
343
|
+
return (
|
|
344
|
+
False,
|
|
345
|
+
"Balance adapter not initialized. Strategy initialization may have failed.",
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
(
|
|
349
|
+
success,
|
|
350
|
+
main_usdt0_balance,
|
|
351
|
+
) = await self.balance_adapter.get_balance(
|
|
352
|
+
token_id=self.usdt_token_info.get("token_id"),
|
|
353
|
+
wallet_address=self._get_main_wallet_address(),
|
|
354
|
+
)
|
|
355
|
+
if not success:
|
|
356
|
+
return (
|
|
357
|
+
False,
|
|
358
|
+
f"Failed to get main wallet USDT0 balance: {main_usdt0_balance}",
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
(
|
|
362
|
+
success,
|
|
363
|
+
main_hype_balance,
|
|
364
|
+
) = await self.balance_adapter.get_balance(
|
|
365
|
+
token_id=self.hype_token_info.get("token_id"),
|
|
366
|
+
wallet_address=self._get_main_wallet_address(),
|
|
367
|
+
)
|
|
368
|
+
if not success:
|
|
369
|
+
return (
|
|
370
|
+
False,
|
|
371
|
+
f"Failed to get main wallet HYPE balance: {main_hype_balance}",
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
main_usdt0_native = main_usdt0_balance / (
|
|
375
|
+
10 ** self.usdt_token_info.get("decimals")
|
|
376
|
+
)
|
|
377
|
+
main_hype_native = main_hype_balance / (
|
|
378
|
+
10 ** self.hype_token_info.get("decimals")
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
if main_token_amount > 0:
|
|
382
|
+
if main_usdt0_native < main_token_amount:
|
|
383
|
+
return (
|
|
384
|
+
False,
|
|
385
|
+
f"Main wallet USDT0 balance is less than the deposit amount: {main_usdt0_native} < {main_token_amount}",
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
if gas_token_amount > 0:
|
|
389
|
+
if main_hype_native < gas_token_amount:
|
|
390
|
+
return (
|
|
391
|
+
False,
|
|
392
|
+
f"Main wallet HYPE balance is less than the deposit amount: {main_hype_native} < {gas_token_amount}",
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
if gas_token_amount > 0:
|
|
396
|
+
(
|
|
397
|
+
success,
|
|
398
|
+
msg,
|
|
399
|
+
) = await self.balance_adapter.move_from_main_wallet_to_strategy_wallet(
|
|
400
|
+
self.hype_token_info.get("token_id"),
|
|
401
|
+
gas_token_amount,
|
|
402
|
+
strategy_name=self.name,
|
|
403
|
+
)
|
|
404
|
+
if not success:
|
|
405
|
+
return (False, f"HYPE transfer to strategy failed: {msg}")
|
|
406
|
+
|
|
407
|
+
if main_token_amount > 0:
|
|
408
|
+
(
|
|
409
|
+
success,
|
|
410
|
+
msg,
|
|
411
|
+
) = await self.balance_adapter.move_from_main_wallet_to_strategy_wallet(
|
|
412
|
+
self.usdt_token_info.get("token_id"),
|
|
413
|
+
main_token_amount,
|
|
414
|
+
strategy_name=self.name,
|
|
415
|
+
)
|
|
416
|
+
if not success:
|
|
417
|
+
return (False, f"USDT0 transfer to strategy failed: {msg}")
|
|
418
|
+
|
|
327
419
|
self._invalidate_assets_snapshot()
|
|
328
420
|
await self._hydrate_position_from_chain()
|
|
329
421
|
|
|
330
|
-
return (
|
|
422
|
+
return (success, msg)
|
|
331
423
|
|
|
332
424
|
async def _estimate_redeploy_tokens(self) -> float:
|
|
333
425
|
positions = await self._get_lent_positions()
|
|
@@ -1344,26 +1436,12 @@ class HyperlendStableYieldStrategy(Strategy):
|
|
|
1344
1436
|
continue
|
|
1345
1437
|
|
|
1346
1438
|
balance_tokens = float(entry.get("tokens") or 0)
|
|
1347
|
-
if balance_tokens
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
transfer_success,
|
|
1353
|
-
_,
|
|
1354
|
-
) = await self.balance_adapter.move_from_strategy_wallet_to_main_wallet(
|
|
1355
|
-
token_id=token.get("token_id"),
|
|
1356
|
-
amount=balance_tokens,
|
|
1357
|
-
strategy_name=self.name,
|
|
1358
|
-
)
|
|
1359
|
-
except Exception:
|
|
1360
|
-
continue
|
|
1361
|
-
|
|
1362
|
-
if transfer_success:
|
|
1363
|
-
actions.append(
|
|
1364
|
-
f"Transferred {balance_tokens:.4f} {token.get('symbol')} to main wallet"
|
|
1439
|
+
if balance_tokens > 0:
|
|
1440
|
+
logger.warning(
|
|
1441
|
+
f"Failed to swap {balance_tokens:.4f} {token.get('symbol', 'unknown')} "
|
|
1442
|
+
f"to {target_token.get('symbol', 'target')} during alignment. "
|
|
1443
|
+
f"Leaving in strategy wallet for next update cycle."
|
|
1365
1444
|
)
|
|
1366
|
-
self._invalidate_assets_snapshot()
|
|
1367
1445
|
|
|
1368
1446
|
kept_tokens = float(balances.get("native", {}).get("tokens") or 0)
|
|
1369
1447
|
|
|
@@ -46,20 +46,20 @@ def strategy():
|
|
|
46
46
|
config=mock_config,
|
|
47
47
|
main_wallet=mock_config["main_wallet"],
|
|
48
48
|
strategy_wallet=mock_config["strategy_wallet"],
|
|
49
|
-
simulation=True,
|
|
50
49
|
)
|
|
51
50
|
|
|
52
51
|
if hasattr(s, "balance_adapter") and s.balance_adapter:
|
|
53
52
|
# Mock balances: 1000 USDT0 (with 6 decimals) and 2 HYPE (with 18 decimals)
|
|
54
|
-
usdt0_balance_mock = AsyncMock(return_value=(True, 1000000000))
|
|
55
|
-
hype_balance_mock = AsyncMock(return_value=(True, 2000000000000000000))
|
|
56
|
-
|
|
57
53
|
def get_balance_side_effect(token_id, wallet_address, **kwargs):
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
return
|
|
62
|
-
|
|
54
|
+
token_id_str = str(token_id).lower() if token_id else ""
|
|
55
|
+
if "usdt0" in token_id_str or token_id_str == "usdt0":
|
|
56
|
+
# 1000 USDT0 with 6 decimals = 1000 * 10^6 = 1000000000
|
|
57
|
+
return (True, 1000000000)
|
|
58
|
+
elif "hype" in token_id_str or token_id_str == "hype":
|
|
59
|
+
# 2 HYPE with 18 decimals = 2 * 10^18 = 2000000000000000000
|
|
60
|
+
return (True, 2000000000000000000)
|
|
61
|
+
# Default: return high balance for any other token
|
|
62
|
+
return (True, 2000000000000000000)
|
|
63
63
|
|
|
64
64
|
s.balance_adapter.get_balance = AsyncMock(side_effect=get_balance_side_effect)
|
|
65
65
|
|
|
@@ -101,15 +101,54 @@ def strategy():
|
|
|
101
101
|
)
|
|
102
102
|
|
|
103
103
|
if hasattr(s, "balance_adapter") and s.balance_adapter:
|
|
104
|
+
# Mock the main methods first
|
|
104
105
|
s.balance_adapter.move_from_main_wallet_to_strategy_wallet = AsyncMock(
|
|
105
106
|
return_value=(True, "Transfer successful (simulated)")
|
|
106
107
|
)
|
|
107
108
|
s.balance_adapter.move_from_strategy_wallet_to_main_wallet = AsyncMock(
|
|
108
109
|
return_value=(True, "Transfer successful (simulated)")
|
|
109
110
|
)
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
111
|
+
# Mock internal dependencies unconditionally to prevent MagicMock await errors
|
|
112
|
+
# These are needed if the real method somehow gets called
|
|
113
|
+
s.balance_adapter.token_client = AsyncMock()
|
|
114
|
+
s.balance_adapter.token_client.get_token_details = AsyncMock(
|
|
115
|
+
return_value={
|
|
116
|
+
"id": "usdt0-hyperevm",
|
|
117
|
+
"address": "0x1234",
|
|
118
|
+
"decimals": 6,
|
|
119
|
+
}
|
|
120
|
+
)
|
|
121
|
+
s.balance_adapter.token_transactions = AsyncMock()
|
|
122
|
+
s.balance_adapter.token_transactions.build_send = AsyncMock(
|
|
123
|
+
return_value=(True, {"transaction": "0xMOCK"})
|
|
124
|
+
)
|
|
125
|
+
s.balance_adapter.wallet_provider = AsyncMock()
|
|
126
|
+
s.balance_adapter.wallet_provider.broadcast_transaction = AsyncMock(
|
|
127
|
+
return_value=(True, {"transaction_hash": "0xCAFEBABE"})
|
|
128
|
+
)
|
|
129
|
+
# token_adapter might already be set, so check before overriding
|
|
130
|
+
if (
|
|
131
|
+
not hasattr(s.balance_adapter, "token_adapter")
|
|
132
|
+
or s.balance_adapter.token_adapter is None
|
|
133
|
+
):
|
|
134
|
+
s.balance_adapter.token_adapter = AsyncMock()
|
|
135
|
+
if hasattr(s.balance_adapter.token_adapter, "get_token_price"):
|
|
136
|
+
s.balance_adapter.token_adapter.get_token_price = AsyncMock(
|
|
137
|
+
return_value=(True, {"current_price": 1.0})
|
|
138
|
+
)
|
|
139
|
+
# ledger_adapter might already be set, so check before overriding
|
|
140
|
+
if (
|
|
141
|
+
not hasattr(s.balance_adapter, "ledger_adapter")
|
|
142
|
+
or s.balance_adapter.ledger_adapter is None
|
|
143
|
+
):
|
|
144
|
+
s.balance_adapter.ledger_adapter = AsyncMock()
|
|
145
|
+
if hasattr(s.balance_adapter.ledger_adapter, "record_deposit"):
|
|
146
|
+
s.balance_adapter.ledger_adapter.record_deposit = AsyncMock(
|
|
147
|
+
return_value=(True, {})
|
|
148
|
+
)
|
|
149
|
+
if hasattr(s.balance_adapter.ledger_adapter, "record_withdrawal"):
|
|
150
|
+
s.balance_adapter.ledger_adapter.record_withdrawal = AsyncMock(
|
|
151
|
+
return_value=(True, {})
|
|
113
152
|
)
|
|
114
153
|
|
|
115
154
|
if hasattr(s, "ledger_adapter") and s.ledger_adapter:
|
|
@@ -304,8 +343,8 @@ async def test_canonical_usage(strategy):
|
|
|
304
343
|
for example_name, example_data in canonical.items():
|
|
305
344
|
if "deposit" in example_data:
|
|
306
345
|
deposit_params = example_data.get("deposit", {})
|
|
307
|
-
ok,
|
|
308
|
-
assert ok, f"Canonical example '{example_name}' deposit failed"
|
|
346
|
+
ok, msg = await strategy.deposit(**deposit_params)
|
|
347
|
+
assert ok, f"Canonical example '{example_name}' deposit failed: {msg}"
|
|
309
348
|
|
|
310
349
|
if "update" in example_data:
|
|
311
350
|
result = await strategy.update()
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Moonwell wstETH Loop Strategy
|
|
2
|
+
|
|
3
|
+
- Entrypoint: `strategies.moonwell_wsteth_loop_strategy.strategy.MoonwellWstethLoopStrategy`
|
|
4
|
+
- Examples: `examples.json`
|
|
5
|
+
- Tests: `test_strategy.py`
|
|
6
|
+
|
|
7
|
+
## What it does
|
|
8
|
+
|
|
9
|
+
A leveraged liquid-staking carry trade on Base that loops USDC collateral into wstETH exposure. The strategy:
|
|
10
|
+
|
|
11
|
+
1. Deposits USDC as initial collateral on Moonwell lending protocol.
|
|
12
|
+
2. Borrows WETH against the USDC collateral.
|
|
13
|
+
3. Swaps WETH to wstETH via Aerodrome/BRAP routing.
|
|
14
|
+
4. Lends wstETH back to Moonwell as additional collateral.
|
|
15
|
+
5. Repeats the loop until target leverage is reached or marginal gains fall below threshold.
|
|
16
|
+
|
|
17
|
+
The position is **delta-neutral**: WETH debt offsets wstETH collateral, so PnL is driven by the spread between wstETH staking yield and WETH borrow cost.
|
|
18
|
+
|
|
19
|
+
## Key parameters
|
|
20
|
+
|
|
21
|
+
- `MIN_GAS = 0.002` ETH (minimum Base ETH for gas)
|
|
22
|
+
- `MIN_USDC_DEPOSIT = 20` USDC (minimum initial collateral)
|
|
23
|
+
- `MAX_DEPEG = 0.01` (1% max stETH/ETH depeg threshold)
|
|
24
|
+
- `MIN_HEALTH_FACTOR = 1.2` (triggers deleveraging if below)
|
|
25
|
+
- `MAX_HEALTH_FACTOR = 1.5` (triggers leverage loop if above)
|
|
26
|
+
- `leverage_limit = 10` (maximum leverage multiplier)
|
|
27
|
+
- `COLLATERAL_SAFETY_FACTOR = 0.98` (2% safety buffer on borrows)
|
|
28
|
+
- `MAX_SLIPPAGE_TOLERANCE = 0.03` (3% max slippage to prevent MEV)
|
|
29
|
+
- `_MIN_LEVERAGE_GAIN_BPS = 50e-4` (stop looping if marginal gain < 50 bps)
|
|
30
|
+
|
|
31
|
+
## Safety features
|
|
32
|
+
|
|
33
|
+
- **Depeg guard**: `_max_safe_F()` calculates leverage ceiling based on wstETH collateral factor and max depeg tolerance.
|
|
34
|
+
- **Delta-neutrality**: `_balance_weth_debt()` rebalances when WETH debt exceeds wstETH collateral value.
|
|
35
|
+
- **Swap retries**: `_swap_with_retries()` uses progressive slippage (0.5% → 1% → 1.5%) with exponential backoff.
|
|
36
|
+
- **Health monitoring**: Automatic deleveraging when health factor drops below `MIN_HEALTH_FACTOR`.
|
|
37
|
+
- **Rollback protection**: Checks actual balances before rollback swaps to prevent failed transactions.
|
|
38
|
+
|
|
39
|
+
## Adapters used
|
|
40
|
+
|
|
41
|
+
- `BalanceAdapter` for token balances and wallet transfers with ledger tracking.
|
|
42
|
+
- `TokenAdapter` for token metadata and price feeds.
|
|
43
|
+
- `LedgerAdapter` for net deposit tracking.
|
|
44
|
+
- `BRAPAdapter` for swap quotes and execution via Aerodrome/routing.
|
|
45
|
+
- `MoonwellAdapter` for lending, borrowing, collateral management, and position queries.
|
|
46
|
+
- `LocalTokenTxnService` via `DefaultWeb3Service` for low-level sends/approvals.
|
|
47
|
+
|
|
48
|
+
## Actions
|
|
49
|
+
|
|
50
|
+
### Deposit
|
|
51
|
+
|
|
52
|
+
- Validates USDC and ETH balances in the main wallet.
|
|
53
|
+
- Transfers ETH (gas) into the strategy wallet if needed.
|
|
54
|
+
- Moves USDC from main wallet to strategy wallet.
|
|
55
|
+
- Lends USDC on Moonwell and enables as collateral.
|
|
56
|
+
- Executes leverage loop: borrow WETH → swap to wstETH → lend wstETH → repeat.
|
|
57
|
+
|
|
58
|
+
### Update
|
|
59
|
+
|
|
60
|
+
- Checks gas balance meets maintenance threshold.
|
|
61
|
+
- Balances WETH debt against wstETH collateral for delta-neutrality.
|
|
62
|
+
- Computes health factor from aggregated positions.
|
|
63
|
+
- If HF < MIN: triggers deleveraging via `_repay_debt_loop()`.
|
|
64
|
+
- If HF > MAX: executes additional leverage loops to optimize yield.
|
|
65
|
+
- Claims WELL rewards if above minimum threshold.
|
|
66
|
+
|
|
67
|
+
### Status
|
|
68
|
+
|
|
69
|
+
`_status()` returns:
|
|
70
|
+
|
|
71
|
+
- `portfolio_value`: sum of all position values (USDC lent + wstETH lent - WETH debt)
|
|
72
|
+
- `net_deposit`: fetched from LedgerAdapter
|
|
73
|
+
- `strategy_status`: includes current leverage, health factor, LTV, peg diff, credit remaining
|
|
74
|
+
|
|
75
|
+
### Withdraw
|
|
76
|
+
|
|
77
|
+
- Sweeps miscellaneous token balances to WETH.
|
|
78
|
+
- Repays all WETH debt via `_repay_debt_loop()`.
|
|
79
|
+
- Unlends wstETH, swaps to USDC.
|
|
80
|
+
- Unlends USDC collateral.
|
|
81
|
+
- Returns USDC and remaining ETH to main wallet.
|
|
82
|
+
|
|
83
|
+
## Running locally
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
# Install dependencies
|
|
87
|
+
poetry install
|
|
88
|
+
|
|
89
|
+
# Generate wallets (writes wallets.json)
|
|
90
|
+
poetry run python wayfinder_paths/scripts/make_wallets.py -n 1
|
|
91
|
+
|
|
92
|
+
# Copy config and edit credentials
|
|
93
|
+
cp wayfinder_paths/config.example.json config.json
|
|
94
|
+
|
|
95
|
+
# Check status / health
|
|
96
|
+
poetry run python wayfinder_paths/run_strategy.py moonwell_wsteth_loop_strategy --action status --config $(pwd)/config.json
|
|
97
|
+
|
|
98
|
+
# Perform a deposit/update/withdraw cycle
|
|
99
|
+
poetry run python wayfinder_paths/run_strategy.py moonwell_wsteth_loop_strategy --action deposit --main-token-amount 100 --gas-token-amount 0.01 --config $(pwd)/config.json
|
|
100
|
+
poetry run python wayfinder_paths/run_strategy.py moonwell_wsteth_loop_strategy --action update --config $(pwd)/config.json
|
|
101
|
+
poetry run python wayfinder_paths/run_strategy.py moonwell_wsteth_loop_strategy --action withdraw --config $(pwd)/config.json
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Testing
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
poetry run pytest wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/ -v
|
|
108
|
+
```
|