1use std::sync::LazyLock;
2
3use alloy::primitives::{U256, utils::parse_ether};
4use chrono::{Months, NaiveDate, NaiveDateTime, NaiveTime};
5use espresso_types::{v0_1::ChainId, v0_3::RewardAmount};
6use serde::Deserialize;
7
8const SCHEDULE_TOML: &str = include_str!("../../../../../data/token-unlock-schedule.toml");
9
10pub(crate) const MAINNET_CHAIN_ID: u64 = 1;
11
12#[derive(Deserialize)]
13struct UnlockEntry {
14 month: u64,
15 amount_esp: u64,
16}
17
18#[derive(Deserialize)]
19struct UnlockSchedule {
20 tge_date: String,
21 unlocks: Vec<UnlockEntry>,
22}
23
24struct UnlockCliff {
25 timestamp: u64,
26 amount_wei: U256,
27}
28
29struct ParsedSchedule {
30 unlocks: Vec<UnlockCliff>,
33}
34
35static SCHEDULE: LazyLock<ParsedSchedule> = LazyLock::new(|| {
36 let schedule: UnlockSchedule =
37 toml::from_str(SCHEDULE_TOML).expect("valid token-unlock-schedule.toml");
38
39 let tge_date =
40 NaiveDate::parse_from_str(&schedule.tge_date, "%Y-%m-%d").expect("valid tge_date");
41
42 assert!(
43 schedule.unlocks.windows(2).all(|w| w[0].month < w[1].month),
44 "unlock schedule must be sorted by month"
45 );
46
47 let unlocks = schedule
48 .unlocks
49 .iter()
50 .map(|entry| {
51 let date = tge_date
52 .checked_add_months(Months::new(
53 u32::try_from(entry.month).expect("month fits in u32"),
54 ))
55 .expect("valid calendar month from TGE");
56 let ts = NaiveDateTime::new(date, NaiveTime::MIN)
57 .and_utc()
58 .timestamp();
59 assert!(ts >= 0, "unlock dates must be after unix epoch");
60 let timestamp = ts as u64;
61 let amount_wei = parse_ether(&entry.amount_esp.to_string()).unwrap();
62 UnlockCliff {
63 timestamp,
64 amount_wei,
65 }
66 })
67 .collect();
68
69 ParsedSchedule { unlocks }
70});
71
72pub fn unlocked_amount_at(timestamp_secs: u64) -> U256 {
77 SCHEDULE
78 .unlocks
79 .iter()
80 .rev()
81 .find(|cliff| timestamp_secs >= cliff.timestamp)
82 .map(|cliff| cliff.amount_wei)
83 .unwrap_or(U256::ZERO)
84}
85
86pub struct SupplyCalculator {
96 chain_id: U256,
97 now_secs: u64,
98 initial_supply: U256,
99 total_supply_l1: U256,
100 total_reward_distributed: U256,
101}
102
103impl SupplyCalculator {
104 pub fn new(
105 chain_id: ChainId,
106 now_secs: u64,
107 initial_supply: U256,
108 total_supply_l1: U256,
109 total_reward_distributed: Option<RewardAmount>,
110 ) -> Self {
111 Self {
112 chain_id: chain_id.0,
113 now_secs,
114 initial_supply,
115 total_supply_l1,
116 total_reward_distributed: total_reward_distributed.map(|r| r.0).unwrap_or(U256::ZERO),
117 }
118 }
119
120 fn locked(&self) -> U256 {
123 if self.chain_id == U256::from(MAINNET_CHAIN_ID) {
124 self.initial_supply
125 .saturating_sub(unlocked_amount_at(self.now_secs))
126 } else {
127 U256::ZERO
128 }
129 }
130
131 pub fn circulating_supply(&self) -> U256 {
134 self.initial_supply
135 .saturating_add(self.total_reward_distributed)
136 .saturating_sub(self.locked())
137 }
138
139 pub fn circulating_supply_ethereum(&self) -> U256 {
142 self.total_supply_l1.saturating_sub(self.locked())
143 }
144
145 pub fn total_issued_supply(&self) -> U256 {
147 self.initial_supply
148 .saturating_add(self.total_reward_distributed)
149 }
150
151 pub fn total_reward_distributed(&self) -> U256 {
153 self.total_reward_distributed
154 }
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160
161 fn month_boundary(n: usize) -> u64 {
162 SCHEDULE.unlocks[n].timestamp
163 }
164
165 fn tge_timestamp() -> u64 {
166 month_boundary(0)
167 }
168
169 #[test]
170 fn test_before_tge() {
171 assert_eq!(unlocked_amount_at(0), U256::ZERO);
172 assert_eq!(unlocked_amount_at(tge_timestamp() - 1), U256::ZERO);
173 }
174
175 #[test]
176 fn test_at_tge() {
177 let expected = parse_ether("520550000").unwrap();
178 assert_eq!(unlocked_amount_at(tge_timestamp()), expected);
179 }
180
181 #[test]
182 fn test_after_last_month() {
183 let far_future = tge_timestamp() + 10 * 365 * 86400; let expected = parse_ether("3590000000").unwrap();
185 assert_eq!(unlocked_amount_at(far_future), expected);
186 }
187
188 #[test]
189 fn test_cliff_mid_month_equals_start_of_month() {
190 let half_month = (month_boundary(0) + month_boundary(1)) / 2;
192 let result = unlocked_amount_at(half_month);
193 let expected = parse_ether("520550000").unwrap();
194 assert_eq!(result, expected);
195 }
196
197 #[test]
198 fn test_cliff_just_before_next_month() {
199 let just_before_month_1 = month_boundary(1) - 1;
201 let expected = parse_ether("520550000").unwrap();
202 assert_eq!(unlocked_amount_at(just_before_month_1), expected);
203 }
204
205 #[test]
206 fn test_at_exact_month_boundary() {
207 let expected = parse_ether("540400476").unwrap();
208 assert_eq!(unlocked_amount_at(month_boundary(1)), expected);
209
210 let expected = parse_ether("1198597668").unwrap();
211 assert_eq!(unlocked_amount_at(month_boundary(12)), expected);
212 }
213
214 #[test]
215 fn test_at_month_72() {
216 let expected = parse_ether("3590000000").unwrap();
217 assert_eq!(unlocked_amount_at(month_boundary(72)), expected);
218 }
219
220 #[test]
221 fn test_schedule_is_monotonically_increasing() {
222 for window in SCHEDULE.unlocks.windows(2) {
223 assert!(window[1].amount_wei >= window[0].amount_wei);
224 assert!(window[1].timestamp > window[0].timestamp);
225 }
226 }
227
228 #[test]
229 fn test_schedule_has_expected_entries() {
230 assert_eq!(SCHEDULE.unlocks.len(), 73);
231 }
232
233 #[test]
234 fn test_tge_date_parses() {
235 use chrono::{Datelike, TimeZone, Utc};
236 let dt = Utc
237 .timestamp_opt(SCHEDULE.unlocks[0].timestamp as i64, 0)
238 .single()
239 .unwrap();
240 assert_eq!(dt.year(), 2026);
241 assert_eq!(dt.month(), 2);
242 assert_eq!(dt.day(), 12);
243 }
244
245 fn mainnet_id() -> ChainId {
248 ChainId(U256::from(MAINNET_CHAIN_ID))
249 }
250
251 fn testnet_id() -> ChainId {
252 ChainId(U256::from(35353u64))
253 }
254
255 fn post_tge_time() -> u64 {
256 month_boundary(6)
257 }
258
259 fn mainnet_initial_supply() -> U256 {
261 parse_ether("3590000000").unwrap()
262 }
263
264 #[test]
265 fn test_locked_mainnet() {
266 let now = post_tge_time();
267 let initial_supply = mainnet_initial_supply();
268 let total_supply_l1 = initial_supply + parse_ether("50").unwrap();
269 let calc = SupplyCalculator::new(mainnet_id(), now, initial_supply, total_supply_l1, None);
270
271 let unlocked = unlocked_amount_at(now);
272 assert_eq!(calc.locked(), initial_supply - unlocked);
273 }
274
275 #[test]
276 fn test_locked_non_mainnet() {
277 let now = post_tge_time();
278 let calc = SupplyCalculator::new(
279 testnet_id(),
280 now,
281 parse_ether("1000").unwrap(),
282 parse_ether("1000").unwrap(),
283 None,
284 );
285 assert_eq!(calc.locked(), U256::ZERO);
286 }
287
288 #[test]
289 fn test_locked_before_tge() {
290 let before_tge = tge_timestamp() - 1;
292 let initial_supply = mainnet_initial_supply();
293 let calc = SupplyCalculator::new(
294 mainnet_id(),
295 before_tge,
296 initial_supply,
297 initial_supply,
298 None,
299 );
300 assert_eq!(calc.locked(), initial_supply);
301 }
302
303 #[test]
304 fn test_locked_after_full_vest() {
305 let far_future = tge_timestamp() + 100 * 365 * 86400;
308 let initial_supply = mainnet_initial_supply();
309 let calc = SupplyCalculator::new(
310 mainnet_id(),
311 far_future,
312 initial_supply,
313 initial_supply,
314 None,
315 );
316 assert_eq!(calc.locked(), U256::ZERO);
317 }
318
319 #[test]
320 fn test_supply_calculator_mainnet_circulating() {
321 let now = post_tge_time();
322 let initial_supply = mainnet_initial_supply();
323 let total_supply_l1 = initial_supply + parse_ether("50").unwrap();
324 let reward = Some(RewardAmount(parse_ether("100").unwrap()));
325 let calc =
326 SupplyCalculator::new(mainnet_id(), now, initial_supply, total_supply_l1, reward);
327
328 let unlocked = unlocked_amount_at(now);
330 assert_eq!(
331 calc.circulating_supply(),
332 unlocked + parse_ether("100").unwrap()
333 );
334 }
335
336 #[test]
337 fn test_supply_calculator_mainnet_ethereum() {
338 let now = post_tge_time();
339 let initial_supply = mainnet_initial_supply();
340 let claimed = parse_ether("50").unwrap();
341 let total_supply_l1 = initial_supply + claimed;
342 let calc = SupplyCalculator::new(mainnet_id(), now, initial_supply, total_supply_l1, None);
343
344 let unlocked = unlocked_amount_at(now);
347 assert_eq!(calc.circulating_supply_ethereum(), unlocked + claimed);
348 }
349
350 #[test]
351 fn test_supply_calculator_non_mainnet_circulating() {
352 let now = post_tge_time();
353 let reward = Some(RewardAmount(parse_ether("200").unwrap()));
354 let calc = SupplyCalculator::new(
356 testnet_id(),
357 now,
358 parse_ether("1000").unwrap(),
359 parse_ether("1000").unwrap(),
360 reward,
361 );
362
363 assert_eq!(calc.circulating_supply(), parse_ether("1200").unwrap());
364 }
365
366 #[test]
367 fn test_supply_calculator_non_mainnet_ethereum() {
368 let now = post_tge_time();
369 let calc = SupplyCalculator::new(
370 testnet_id(),
371 now,
372 parse_ether("5000").unwrap(),
373 parse_ether("5000").unwrap(),
374 Some(RewardAmount(parse_ether("10").unwrap())),
375 );
376
377 assert_eq!(
379 calc.circulating_supply_ethereum(),
380 parse_ether("5000").unwrap()
381 );
382 }
383
384 #[test]
385 fn test_supply_calculator_invariant() {
386 for chain_id in [mainnet_id(), testnet_id()] {
389 let now = post_tge_time();
390 let initial_supply = parse_ether("10000").unwrap();
391 let reward = parse_ether("500").unwrap();
392 let total_supply_l1 = parse_ether("10200").unwrap();
393 let calc = SupplyCalculator::new(
394 chain_id,
395 now,
396 initial_supply,
397 total_supply_l1,
398 Some(RewardAmount(reward)),
399 );
400
401 assert_eq!(
405 calc.circulating_supply() - calc.circulating_supply_ethereum(),
406 initial_supply + reward - total_supply_l1,
407 );
408 }
409 }
410
411 #[test]
412 fn test_supply_calculator_zero_rewards() {
413 let now = post_tge_time();
414 let initial_supply = mainnet_initial_supply();
415 let calc = SupplyCalculator::new(mainnet_id(), now, initial_supply, initial_supply, None);
416
417 let unlocked = unlocked_amount_at(now);
418 assert_eq!(calc.circulating_supply(), unlocked);
419 }
420
421 #[test]
422 fn test_supply_calculator_zero_claimed() {
423 let now = post_tge_time();
424 let initial_supply = parse_ether("1000").unwrap();
425 let reward = Some(RewardAmount(parse_ether("100").unwrap()));
426 let calc = SupplyCalculator::new(testnet_id(), now, initial_supply, initial_supply, reward);
427 assert_eq!(calc.circulating_supply(), parse_ether("1100").unwrap());
428 }
429
430 #[test]
431 fn test_supply_calculator_no_underflow() {
432 let before_tge = tge_timestamp() - 1;
434 let calc = SupplyCalculator::new(
435 mainnet_id(),
436 before_tge,
437 parse_ether("100").unwrap(),
438 parse_ether("100").unwrap(),
439 None,
440 );
441 assert_eq!(calc.circulating_supply(), U256::ZERO);
442 }
443
444 #[test]
445 fn test_locked_saturates_when_initial_less_than_unlocked() {
446 let now = post_tge_time();
448 let small_initial = parse_ether("100").unwrap();
449 let calc = SupplyCalculator::new(mainnet_id(), now, small_initial, small_initial, None);
450 assert_eq!(calc.locked(), U256::ZERO);
451 assert_eq!(calc.circulating_supply(), small_initial);
453 }
454
455 #[test]
456 fn test_total_issued_supply_with_rewards() {
457 let now = post_tge_time();
458 let initial_supply = mainnet_initial_supply();
459 let reward = Some(RewardAmount(parse_ether("100").unwrap()));
460 let calc = SupplyCalculator::new(mainnet_id(), now, initial_supply, initial_supply, reward);
461 assert_eq!(
462 calc.total_issued_supply(),
463 initial_supply + parse_ether("100").unwrap()
464 );
465 }
466
467 #[test]
468 fn test_total_issued_supply_zero_rewards() {
469 let now = post_tge_time();
470 let initial_supply = mainnet_initial_supply();
471 let calc = SupplyCalculator::new(mainnet_id(), now, initial_supply, initial_supply, None);
472 assert_eq!(calc.total_issued_supply(), initial_supply);
473 }
474
475 #[test]
476 fn test_total_issued_supply_chain_invariant() {
477 let now = post_tge_time();
478 let initial_supply = parse_ether("10000").unwrap();
479 let reward = Some(RewardAmount(parse_ether("500").unwrap()));
480 let total_supply_l1 = parse_ether("10200").unwrap();
481
482 let mainnet =
483 SupplyCalculator::new(mainnet_id(), now, initial_supply, total_supply_l1, reward);
484 let testnet =
485 SupplyCalculator::new(testnet_id(), now, initial_supply, total_supply_l1, reward);
486
487 assert_eq!(mainnet.total_issued_supply(), testnet.total_issued_supply());
488 }
489
490 #[test]
491 fn test_total_reward_distributed() {
492 let now = post_tge_time();
493 let initial_supply = mainnet_initial_supply();
494 let reward = Some(RewardAmount(parse_ether("200").unwrap()));
495 let calc = SupplyCalculator::new(mainnet_id(), now, initial_supply, initial_supply, reward);
496 assert_eq!(calc.total_reward_distributed(), parse_ether("200").unwrap());
497 }
498
499 #[test]
500 fn test_total_reward_distributed_zero() {
501 let now = post_tge_time();
502 let initial_supply = mainnet_initial_supply();
503 let calc = SupplyCalculator::new(mainnet_id(), now, initial_supply, initial_supply, None);
504 assert_eq!(calc.total_reward_distributed(), U256::ZERO);
505 }
506}