Skip to content

Commit cbbf496

Browse files
divclaude
andcommitted
propeller M3: crash+rebalance deleverTarget queue-priority scenario; M2 harvestThreshold fidelity (0.1% per SubLoop.sol:157)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 1c73b16 commit cbbf496

5 files changed

Lines changed: 100 additions & 2 deletions

File tree

hydradx/apps/propeller/m2_model.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ def __init__(self,
107107
deploy_tranche=5_000.0,
108108
unwind_tranche=5_000.0,
109109
pokes_per_step=None, # keeper pokes per step (None = every block)
110-
harvest_threshold=0.005, # skim once surplus > 0.5% of basis
110+
harvest_threshold=0.001, # skim once surplus > 0.1% of basis (SubLoop.sol:157 default)
111111
slippage_band=0.01,
112112
f1_fixed=False, # True: harvest reserves accrued Main interest
113113
ltv=ETH_LTV):
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
S1 baseline 1y [as-implemented]:
2+
backing gap: day30 $13,003 day180 $81,714 day365 $159,747
3+
compounded yield $594,373 (11.89% of deposit)
4+
redemption: collateral released 95.64% STUCK 4.36% (≈ $243,709 of user value)
5+
S1 baseline 1y [F1-fixed]:
6+
backing gap: day30 $-29,257 day180 $-28,718 day365 $-35,421
7+
compounded yield $570,216 (11.40% of deposit)
8+
redemption: collateral released 100.00% STUCK 0.00% (≈ $0 of user value)
9+
S2 HOLLAR 7.0% (spread −0.66%): after 180d inverted — gap $125,135 (3.21% of Main debt), HF 1.050, liqs 0
10+
S2 HOLLAR 10% (spread −3.66%): after 180d inverted — gap $459,824 (11.63% of Main debt), HF 1.050, liqs 0
11+
S3 1%/day to 5% : liqs 0 liq-loss $ 0 equity Δ $ -1,035,704 final HF 1.050 gap $1,047,160
12+
S3 5% over 12h : liqs 0 liq-loss $ 0 equity Δ $ -1,018,050 final HF 1.050 gap $1,027,470
13+
S3 5% over 2h : liqs 0 liq-loss $ 0 equity Δ $ -1,020,810 final HF 1.050 gap $1,030,060
14+
S3 5% instant : liqs 1 liq-loss $ 677,913 equity Δ $ -1,833,506 final HF 1.050 gap $1,842,700
15+
S3 8% over 12h : liqs 0 liq-loss $ 0 equity Δ $ -1,508,851 final HF 1.050 gap $1,518,289
16+
S3 8% instant : liqs 2 liq-loss $1,016,870 equity Δ $ -2,865,850 final HF 1.050 gap $2,875,082
17+
S3 12% over 24h : liqs 0 liq-loss $ 0 equity Δ $ -2,046,232 final HF 1.050 gap $2,055,878
18+
S4 rate 4.4%: synth floor breaks after 42d keeper outage; at outage+30d Main liquidates if collateral falls 99.7%
19+
S4 rate 10%: synth floor breaks after 19d keeper outage; at outage+30d Main liquidates if collateral falls 99.2%
20+
S4 rate 20%: synth floor breaks after 10d keeper outage; at outage+30d Main liquidates if collateral falls 98.5%
21+
S5 exit during 3% PRIME discount: released 82.08% of deposit, stuck 17.92%, liqs 0, gap $654,514

hydradx/apps/propeller/m3_model.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ def __init__(self, entry_fee=0.0, exit_fee=0.0, arb_capacity_per_hour=None, **kw
4040
self._arb_budget = 0.0
4141
self.shares = {} # depositor -> vault shares
4242
self.total_shares = 0.0
43+
self.collateral_price_mult = 1.0 # collateral price factor (1.0 = entry price)
44+
self.vault_delever_target = 0.0 # Main debt to repay from a down-rebalance
45+
# (settles AHEAD of the redemption queue)
4346
self.fee_pot = 0.0 # fees retained (accrue to remaining holders via collateral)
4447
self.queue = [] # FIFO: dicts(owner, shares, coll_owed, debt_share, repaid, coll_settled)
4548

@@ -85,11 +88,38 @@ def request_redeem(self, owner, frac=1.0):
8588
self.queue.append(req)
8689
return req
8790

91+
def crash_and_rebalance(self, drop: float):
92+
"""Collateral price falls by `drop`; permissionless rebalance() fires
93+
(CollateralVault.sol:481-496): unwind the loop slice worth the excess
94+
debt; its freed HOLLAR repays Main debt AHEAD of the redemption queue."""
95+
self.collateral_price_mult *= (1 - drop)
96+
self.collateral_usd *= (1 - drop)
97+
max_ltv = 0.75
98+
if self.collateral_usd <= 0:
99+
return 0.0
100+
ltv = self.main_debt / self.collateral_usd
101+
if ltv <= max_ltv + 0.03: # +300bp hysteresis band
102+
return 0.0
103+
repay = self.main_debt - self.collateral_usd * max_ltv
104+
equity = max(self.loop_collateral_usd() - self.loop_debt, 0.0)
105+
slice_eq = min(repay, equity)
106+
self.unwind_target += slice_eq
107+
self.principal_equity = max(self.principal_equity - slice_eq, 0.0)
108+
self.vault_delever_target += repay
109+
return repay
110+
88111
def poke_settle(self):
89-
"""FIFO settlement from freed HOLLAR (+ interest reserve when F1-fixed)."""
112+
"""Settlement from freed HOLLAR: down-rebalance deleverTarget FIRST
113+
(CollateralVault.sol:334-348), then the FIFO queue."""
90114
available = self.freed
91115
if self.f1_fixed and self.interest_reserve > 0:
92116
available += self.interest_reserve
117+
if self.vault_delever_target > 0 and available > 0:
118+
r = min(available, self.vault_delever_target)
119+
available -= r
120+
self.vault_delever_target -= r
121+
self.main_debt -= r
122+
self.synth = self.main_debt / 0.98 * 1.005
93123
for req in self.queue:
94124
if req['done'] or available <= 0:
95125
continue
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
S1 run, discount persists:
2+
queue pos 0: realized $500,000 / $500k stuck $0 settled in 0.04d
3+
queue pos 2: realized $431,457 / $500k stuck $0 settled in 0.08d
4+
queue pos 4: realized $341,869 / $500k stuck $0 settled in 0.08d
5+
queue pos 6: realized $288,175 / $500k stuck $0 settled in 0.12d
6+
queue pos 9: realized $0 / $500k stuck $226,174 settled in NEVER
7+
first-vs-last gap: 100.00pp of deposit
8+
S1 run, discount reverts at d14:
9+
queue pos 0: realized $500,000 / $500k stuck $0 settled in 0.04d
10+
queue pos 2: realized $431,457 / $500k stuck $0 settled in 0.08d
11+
queue pos 4: realized $341,869 / $500k stuck $0 settled in 0.08d
12+
queue pos 6: realized $288,175 / $500k stuck $0 settled in 0.12d
13+
queue pos 9: realized $0 / $500k stuck $226,174 settled in NEVER
14+
first-vs-last gap: 100.00pp of deposit
15+
S2 fee 0bp: 10 cycles — holders +$12,611, attacker cost $69,827 (1.40% per cycle notional)
16+
S2 fee 10bp: 10 cycles — holders +$22,136, attacker cost $76,002 (1.52% per cycle notional)
17+
S2 fee 20bp: 10 cycles — holders +$33,538, attacker cost $89,254 (1.79% per cycle notional)
18+
S2 fee 30bp: 10 cycles — holders +$42,204, attacker cost $96,637 (1.93% per cycle notional)
19+
S2 fee 50bp: 10 cycles — holders +$56,931, attacker cost $114,833 (2.30% per cycle notional)
20+
S3 arb capacity unlimited: settled 4/5; per-request ['0.04d', '0.08d', '0.12d', '0.17d', 'NEVER']
21+
S3 arb capacity $500k/h: settled 4/5; per-request ['0.04d', '0.08d', '0.12d', '0.17d', 'NEVER']
22+
S3 arb capacity $100k/h: settled 4/5; per-request ['0.88d', '1.79d', '2.75d', '3.71d', 'NEVER']
23+
S3 arb capacity $25k/h: settled 4/5; per-request ['0.21d', '1.12d', '2.04d', '2.96d', 'NEVER']
24+
S4 no rebalance (control): settle times ['0.04d', '0.08d', '0.12d', '0.17d', 'NEVER'], residual deleverTarget $0
25+
S4 crash+rebalance (deleverTarget $1,309,615): settle times ['0.04d', '0.21d', '0.25d', '0.25d', 'NEVER'], residual deleverTarget $0

hydradx/apps/propeller/run_m3.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,30 @@ def s3_exit_latency():
9494
print(f"S3 arb capacity {label:>9}: settled {done}/5; per-request {times}")
9595

9696

97+
def s4_crash_rebalance_vs_queue():
98+
"""Collateral −40% + permissionless rebalance() while 5 redemptions are queued:
99+
the vault's deleverTarget settles AHEAD of the queue (CollateralVault.sol:334).
100+
Compare queue settle times with vs without the rebalance call."""
101+
for do_rebalance in (False, True):
102+
w = setup(n_depositors=10, each=500_000)
103+
for _ in range(7 * H):
104+
w.step(600)
105+
reqs = [w.request_redeem(f'u{i}') for i in range(5)]
106+
w.step(600)
107+
if do_rebalance:
108+
repay = w.crash_and_rebalance(0.40)
109+
for _ in range(30 * 24):
110+
w.step(600)
111+
if all(r['done'] for r in reqs):
112+
break
113+
times = [f"{(r['done_block']-r['requested_block'])/BLOCKS_PER_DAY:.2f}d" if r['done_block'] else 'NEVER' for r in reqs]
114+
label = f"crash+rebalance (deleverTarget ${repay:,.0f})" if do_rebalance else "no rebalance (control)"
115+
print(f"S4 {label}: settle times {times}, residual deleverTarget ${w.vault_delever_target:,.0f}")
116+
117+
97118
if __name__ == '__main__':
98119
s1_stress_run(reverts=False)
99120
s1_stress_run(reverts=True)
100121
s2_churn_attack()
101122
s3_exit_latency()
123+
s4_crash_rebalance_vs_queue()

0 commit comments

Comments
 (0)