-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathvulnerable_bot.py
More file actions
219 lines (184 loc) · 9.09 KB
/
vulnerable_bot.py
File metadata and controls
219 lines (184 loc) · 9.09 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
"""
vulnerable_bot.py — RWA Liquidation Bot with Hardcoded NYSE Hours
This bot demonstrates the EXACT failure that will occur after March 8, 2026
when US clocks spring forward (EST -> EDT). Any bot using a hardcoded UTC
offset to compute NYSE trading hours will miscalculate for every day until
the offset is manually corrected.
The DST transition happens at 2:00 AM ET on Sunday, March 8. The first
trading day affected is Monday, March 9. Every time-based decision the bot
makes on that day (and beyond) is wrong by one hour.
Run: python vulnerable_bot.py
"""
from datetime import datetime, timezone, timedelta
# --- Configuration ---------------------------------------------------------
COLLATERAL_ASSET = "OUSG" # Ondo Finance tokenized US Treasuries
COLLATERAL_VALUE_USD = 100_000_000 # $100M position (OUSG TVL is $1.3B Jan 2026)
COLLATERALIZATION_RATIO = 1.50 # 150% (typical RWA lending protocol)
OVERNIGHT_DROP_PCT = 0.15 # 15% after-hours drop on underlying
# --- The Vulnerable Logic --------------------------------------------------
# This is the pattern used by most RWA liquidation bots today.
# It uses a HARDCODED UTC offset (-5 for EST) to determine NYSE hours.
EST_OFFSET = timedelta(hours=-5) # Bug: assumes EST year-round
def get_nyse_time_vulnerable(utc_time: datetime) -> datetime:
"""Convert UTC to 'Eastern Time' using a hardcoded -5 offset."""
return utc_time + EST_OFFSET
def is_nyse_open_vulnerable(utc_time: datetime) -> bool:
"""Check if NYSE is open using hardcoded offset logic."""
et = get_nyse_time_vulnerable(utc_time)
# Weekend check
if et.weekday() >= 5:
return False
# Market hours: 9:30 AM - 4:00 PM ET
market_open = et.replace(hour=9, minute=30, second=0, microsecond=0)
market_close = et.replace(hour=16, minute=0, second=0, microsecond=0)
return market_open <= et < market_close
def check_liquidation(utc_time: datetime, nav_drop_pct: float) -> dict:
"""Decide whether to liquidate based on collateral health."""
current_value = COLLATERAL_VALUE_USD * (1 - nav_drop_pct)
debt_value = COLLATERAL_VALUE_USD / COLLATERALIZATION_RATIO
health_factor = current_value / debt_value
result = {
"timestamp_utc": utc_time.isoformat(),
"eastern_time": get_nyse_time_vulnerable(utc_time).strftime("%Y-%m-%d %H:%M ET"),
"collateral_value": f"${current_value:,.0f}",
"debt_value": f"${debt_value:,.0f}",
"health_factor": round(health_factor, 3),
"liquidation_triggered": False,
"error": None,
}
if health_factor < 1.0:
# Bot thinks it should liquidate
if is_nyse_open_vulnerable(utc_time):
result["liquidation_triggered"] = True
result["action"] = "LIQUIDATE -- Market believed open"
else:
result["action"] = "DEFER -- Market believed closed"
else:
result["action"] = "HOLD -- Health factor above 1.0"
return result
# --- Simulation ------------------------------------------------------------
def run_simulation():
print("=" * 72)
print("VULNERABLE BOT -- Hardcoded UTC Offset Liquidation Logic")
print("=" * 72)
print()
# -- Explanation --------------------------------------------------------
print("BACKGROUND")
print("-" * 72)
print(" DST transition: Sunday, March 8, 2026 at 2:00 AM ET")
print(" Clocks spring forward: 2:00 AM -> 3:00 AM (EST -> EDT)")
print(" UTC offset changes from -5 (EST) to -4 (EDT)")
print()
print(" This bot uses a HARDCODED offset of UTC-5 (EST).")
print(" After DST, every time calculation is wrong by 1 hour.")
print(" The first trading day affected: Monday, March 9, 2026.")
print()
# -- Scenario 1: Normal day (before DST) --------------------------------
print("SCENARIO 1: Normal trading day BEFORE DST (Friday, March 6)")
print("-" * 72)
# 2:30 PM ET on March 6 (EST) = 7:30 PM UTC (UTC-5 is correct)
normal_day = datetime(2026, 3, 6, 19, 30, tzinfo=timezone.utc)
bot_et = get_nyse_time_vulnerable(normal_day)
bot_open = is_nyse_open_vulnerable(normal_day)
print(f" UTC Time: {normal_day.strftime('%Y-%m-%d %H:%M UTC')}")
print(f" Bot's ET Calc: {bot_et.strftime('%H:%M')} (using UTC-5)")
print(f" Actual ET: 14:30 EST (UTC-5)")
print(f" Bot correct? YES -- EST offset is correct before March 8")
print(f" Bot thinks open? {'YES' if bot_open else 'NO'}")
print()
# -- Scenario 2: Monday March 9 -- first trading day AFTER DST ----------
print("SCENARIO 2: First trading day AFTER DST (Monday, March 9)")
print("-" * 72)
print()
print(" After DST, the correct offset is UTC-4 (EDT), not UTC-5 (EST).")
print(" The bot still uses UTC-5. Every calculation is 1 hour behind.")
print()
test_times = [
# (UTC time, actual ET, explanation)
(
datetime(2026, 3, 9, 13, 30, tzinfo=timezone.utc),
"9:30 AM EDT -- Market OPENS",
"Bot calculates 8:30 AM (UTC-5). Thinks market CLOSED. Misses open by 1hr.",
),
(
datetime(2026, 3, 9, 14, 0, tzinfo=timezone.utc),
"10:00 AM EDT -- Market OPEN",
"Bot calculates 9:00 AM (UTC-5). Still thinks market CLOSED.",
),
(
datetime(2026, 3, 9, 14, 30, tzinfo=timezone.utc),
"10:30 AM EDT -- Market OPEN",
"Bot calculates 9:30 AM (UTC-5). Finally thinks market opened. 1hr LATE.",
),
(
datetime(2026, 3, 9, 20, 0, tzinfo=timezone.utc),
"4:00 PM EDT -- Market CLOSES",
"Bot calculates 3:00 PM (UTC-5). Thinks market STILL OPEN. WRONG.",
),
(
datetime(2026, 3, 9, 20, 30, tzinfo=timezone.utc),
"4:30 PM EDT -- Market CLOSED for 30 min",
"Bot calculates 3:30 PM (UTC-5). STILL thinks market open. DANGEROUS.",
),
(
datetime(2026, 3, 9, 21, 0, tzinfo=timezone.utc),
"5:00 PM EDT -- Market CLOSED for 1 hour",
"Bot calculates 4:00 PM (UTC-5). Just now thinks market closed. 1hr LATE.",
),
]
for utc_time, actual_et, explanation in test_times:
bot_et = get_nyse_time_vulnerable(utc_time)
bot_open = is_nyse_open_vulnerable(utc_time)
print(f" UTC: {utc_time.strftime('%H:%M UTC')}")
print(f" Actual ET: {actual_et}")
print(f" Bot's ET Calc: {bot_et.strftime('%H:%M')} (using hardcoded UTC-5)")
print(f" Bot thinks open? {'YES' if bot_open else 'NO'}")
print(f" Issue: {explanation}")
print()
# -- The Critical Failure -----------------------------------------------
print("=" * 72)
print("THE CRITICAL FAILURE")
print("=" * 72)
print()
print(" Monday March 9, 4:30 PM EDT (market closed 30 minutes ago).")
print(" OUSG has dropped 25% in a flash crash.")
print(" The bot thinks it's 3:30 PM EST -- market still open.")
print()
# 4:30 PM EDT = 20:30 UTC. Bot thinks it's 15:30 EST (in market hours).
critical_time = datetime(2026, 3, 9, 20, 30, tzinfo=timezone.utc)
result = check_liquidation(critical_time, OVERNIGHT_DROP_PCT)
bot_et = get_nyse_time_vulnerable(critical_time)
print(f" UTC Time: {critical_time.strftime('%H:%M UTC')}")
print(f" Actual time: 4:30 PM EDT (market CLOSED at 4:00 PM)")
print(f" Bot calculates: {bot_et.strftime('%H:%M')} EST (thinks market OPEN)")
print(f" OUSG drop: {OVERNIGHT_DROP_PCT*100:.0f}%")
print(f" Collateral value: {result['collateral_value']}")
print(f" Debt outstanding: {result['debt_value']}")
print(f" Health factor: {result['health_factor']}")
print(f" Bot's decision: {result['action']}")
print()
if result["liquidation_triggered"]:
bad_debt = (COLLATERAL_VALUE_USD / COLLATERALIZATION_RATIO) - (COLLATERAL_VALUE_USD * (1 - OVERNIGHT_DROP_PCT))
print(" >>> LIQUIDATION FIRES AGAINST A CLOSED NYSE <<<")
print()
print(" What happens next:")
print(" 1. On-chain liquidation executes at a STALE NAV")
print(" 2. The underlying (US Treasuries) cannot be sold -- NYSE closed")
print(" 3. Settlement fails or executes at a dislocated price")
print(f" 4. Protocol absorbs ${abs(bad_debt):,.0f} in bad debt")
print()
print(" Root cause: The bot used UTC-5 (EST) after DST changed the")
print(" offset to UTC-4 (EDT). It thought 4:30 PM EDT was 3:30 PM EST")
print(" -- still within market hours. One hour of miscalculation.")
print()
print(" This is not a one-time glitch. The bot will be wrong by 1 hour")
print(" for EVERY trading session until the offset is manually fixed.")
else:
print(" Bot did not liquidate (health factor above 1.0 or market closed).")
print()
print("=" * 72)
print("FIX: Use a signed market-status oracle instead of hardcoded offsets.")
print("See safe_bot.py for the 3-line integration that prevents this.")
print("=" * 72)
if __name__ == "__main__":
run_simulation()