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 {
95 chain_id: U256,
96 now_secs: u64,
97 initial_supply: U256,
98 total_supply_l1: U256,
99 total_reward_distributed: U256,
100}
101
102impl SupplyCalculator {
103 pub fn new(
104 chain_id: ChainId,
105 now_secs: u64,
106 initial_supply: U256,
107 total_supply_l1: U256,
108 total_reward_distributed: Option<RewardAmount>,
109 ) -> Self {
110 Self {
111 chain_id: chain_id.0,
112 now_secs,
113 initial_supply,
114 total_supply_l1,
115 total_reward_distributed: total_reward_distributed.map(|r| r.0).unwrap_or(U256::ZERO),
116 }
117 }
118
119 fn locked(&self) -> U256 {
122 if self.chain_id == U256::from(MAINNET_CHAIN_ID) {
123 self.initial_supply
124 .saturating_sub(unlocked_amount_at(self.now_secs))
125 } else {
126 U256::ZERO
127 }
128 }
129
130 pub fn circulating_supply(&self) -> U256 {
133 self.initial_supply
134 .saturating_add(self.total_reward_distributed)
135 .saturating_sub(self.locked())
136 }
137
138 pub fn circulating_supply_ethereum(&self) -> U256 {
141 self.total_supply_l1.saturating_sub(self.locked())
142 }
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148
149 fn month_boundary(n: usize) -> u64 {
150 SCHEDULE.unlocks[n].timestamp
151 }
152
153 fn tge_timestamp() -> u64 {
154 month_boundary(0)
155 }
156
157 #[test]
158 fn test_before_tge() {
159 assert_eq!(unlocked_amount_at(0), U256::ZERO);
160 assert_eq!(unlocked_amount_at(tge_timestamp() - 1), U256::ZERO);
161 }
162
163 #[test]
164 fn test_at_tge() {
165 let expected = parse_ether("520550000").unwrap();
166 assert_eq!(unlocked_amount_at(tge_timestamp()), expected);
167 }
168
169 #[test]
170 fn test_after_last_month() {
171 let far_future = tge_timestamp() + 10 * 365 * 86400; let expected = parse_ether("3590000000").unwrap();
173 assert_eq!(unlocked_amount_at(far_future), expected);
174 }
175
176 #[test]
177 fn test_cliff_mid_month_equals_start_of_month() {
178 let half_month = (month_boundary(0) + month_boundary(1)) / 2;
180 let result = unlocked_amount_at(half_month);
181 let expected = parse_ether("520550000").unwrap();
182 assert_eq!(result, expected);
183 }
184
185 #[test]
186 fn test_cliff_just_before_next_month() {
187 let just_before_month_1 = month_boundary(1) - 1;
189 let expected = parse_ether("520550000").unwrap();
190 assert_eq!(unlocked_amount_at(just_before_month_1), expected);
191 }
192
193 #[test]
194 fn test_at_exact_month_boundary() {
195 let expected = parse_ether("540400476").unwrap();
196 assert_eq!(unlocked_amount_at(month_boundary(1)), expected);
197
198 let expected = parse_ether("1198597668").unwrap();
199 assert_eq!(unlocked_amount_at(month_boundary(12)), expected);
200 }
201
202 #[test]
203 fn test_at_month_72() {
204 let expected = parse_ether("3590000000").unwrap();
205 assert_eq!(unlocked_amount_at(month_boundary(72)), expected);
206 }
207
208 #[test]
209 fn test_schedule_is_monotonically_increasing() {
210 for window in SCHEDULE.unlocks.windows(2) {
211 assert!(window[1].amount_wei >= window[0].amount_wei);
212 assert!(window[1].timestamp > window[0].timestamp);
213 }
214 }
215
216 #[test]
217 fn test_schedule_has_expected_entries() {
218 assert_eq!(SCHEDULE.unlocks.len(), 73);
219 }
220
221 #[test]
222 fn test_tge_date_parses() {
223 use chrono::{Datelike, TimeZone, Utc};
224 let dt = Utc
225 .timestamp_opt(SCHEDULE.unlocks[0].timestamp as i64, 0)
226 .single()
227 .unwrap();
228 assert_eq!(dt.year(), 2026);
229 assert_eq!(dt.month(), 2);
230 assert_eq!(dt.day(), 12);
231 }
232
233 fn mainnet_id() -> ChainId {
236 ChainId(U256::from(MAINNET_CHAIN_ID))
237 }
238
239 fn testnet_id() -> ChainId {
240 ChainId(U256::from(35353u64))
241 }
242
243 fn post_tge_time() -> u64 {
244 month_boundary(6)
245 }
246
247 fn mainnet_initial_supply() -> U256 {
249 parse_ether("3590000000").unwrap()
250 }
251
252 #[test]
253 fn test_locked_mainnet() {
254 let now = post_tge_time();
255 let initial_supply = mainnet_initial_supply();
256 let total_supply_l1 = initial_supply + parse_ether("50").unwrap();
257 let calc = SupplyCalculator::new(mainnet_id(), now, initial_supply, total_supply_l1, None);
258
259 let unlocked = unlocked_amount_at(now);
260 assert_eq!(calc.locked(), initial_supply - unlocked);
261 }
262
263 #[test]
264 fn test_locked_non_mainnet() {
265 let now = post_tge_time();
266 let calc = SupplyCalculator::new(
267 testnet_id(),
268 now,
269 parse_ether("1000").unwrap(),
270 parse_ether("1000").unwrap(),
271 None,
272 );
273 assert_eq!(calc.locked(), U256::ZERO);
274 }
275
276 #[test]
277 fn test_locked_before_tge() {
278 let before_tge = tge_timestamp() - 1;
280 let initial_supply = mainnet_initial_supply();
281 let calc = SupplyCalculator::new(
282 mainnet_id(),
283 before_tge,
284 initial_supply,
285 initial_supply,
286 None,
287 );
288 assert_eq!(calc.locked(), initial_supply);
289 }
290
291 #[test]
292 fn test_locked_after_full_vest() {
293 let far_future = tge_timestamp() + 100 * 365 * 86400;
296 let initial_supply = mainnet_initial_supply();
297 let calc = SupplyCalculator::new(
298 mainnet_id(),
299 far_future,
300 initial_supply,
301 initial_supply,
302 None,
303 );
304 assert_eq!(calc.locked(), U256::ZERO);
305 }
306
307 #[test]
308 fn test_supply_calculator_mainnet_circulating() {
309 let now = post_tge_time();
310 let initial_supply = mainnet_initial_supply();
311 let total_supply_l1 = initial_supply + parse_ether("50").unwrap();
312 let reward = Some(RewardAmount(parse_ether("100").unwrap()));
313 let calc =
314 SupplyCalculator::new(mainnet_id(), now, initial_supply, total_supply_l1, reward);
315
316 let unlocked = unlocked_amount_at(now);
318 assert_eq!(
319 calc.circulating_supply(),
320 unlocked + parse_ether("100").unwrap()
321 );
322 }
323
324 #[test]
325 fn test_supply_calculator_mainnet_ethereum() {
326 let now = post_tge_time();
327 let initial_supply = mainnet_initial_supply();
328 let claimed = parse_ether("50").unwrap();
329 let total_supply_l1 = initial_supply + claimed;
330 let calc = SupplyCalculator::new(mainnet_id(), now, initial_supply, total_supply_l1, None);
331
332 let unlocked = unlocked_amount_at(now);
335 assert_eq!(calc.circulating_supply_ethereum(), unlocked + claimed);
336 }
337
338 #[test]
339 fn test_supply_calculator_non_mainnet_circulating() {
340 let now = post_tge_time();
341 let reward = Some(RewardAmount(parse_ether("200").unwrap()));
342 let calc = SupplyCalculator::new(
344 testnet_id(),
345 now,
346 parse_ether("1000").unwrap(),
347 parse_ether("1000").unwrap(),
348 reward,
349 );
350
351 assert_eq!(calc.circulating_supply(), parse_ether("1200").unwrap());
352 }
353
354 #[test]
355 fn test_supply_calculator_non_mainnet_ethereum() {
356 let now = post_tge_time();
357 let calc = SupplyCalculator::new(
358 testnet_id(),
359 now,
360 parse_ether("5000").unwrap(),
361 parse_ether("5000").unwrap(),
362 Some(RewardAmount(parse_ether("10").unwrap())),
363 );
364
365 assert_eq!(
367 calc.circulating_supply_ethereum(),
368 parse_ether("5000").unwrap()
369 );
370 }
371
372 #[test]
373 fn test_supply_calculator_invariant() {
374 for chain_id in [mainnet_id(), testnet_id()] {
377 let now = post_tge_time();
378 let initial_supply = parse_ether("10000").unwrap();
379 let reward = parse_ether("500").unwrap();
380 let total_supply_l1 = parse_ether("10200").unwrap();
381 let calc = SupplyCalculator::new(
382 chain_id,
383 now,
384 initial_supply,
385 total_supply_l1,
386 Some(RewardAmount(reward)),
387 );
388
389 assert_eq!(
393 calc.circulating_supply() - calc.circulating_supply_ethereum(),
394 initial_supply + reward - total_supply_l1,
395 );
396 }
397 }
398
399 #[test]
400 fn test_supply_calculator_zero_rewards() {
401 let now = post_tge_time();
402 let initial_supply = mainnet_initial_supply();
403 let calc = SupplyCalculator::new(mainnet_id(), now, initial_supply, initial_supply, None);
404
405 let unlocked = unlocked_amount_at(now);
406 assert_eq!(calc.circulating_supply(), unlocked);
407 }
408
409 #[test]
410 fn test_supply_calculator_zero_claimed() {
411 let now = post_tge_time();
412 let initial_supply = parse_ether("1000").unwrap();
413 let reward = Some(RewardAmount(parse_ether("100").unwrap()));
414 let calc = SupplyCalculator::new(testnet_id(), now, initial_supply, initial_supply, reward);
415 assert_eq!(calc.circulating_supply(), parse_ether("1100").unwrap());
416 }
417
418 #[test]
419 fn test_supply_calculator_no_underflow() {
420 let before_tge = tge_timestamp() - 1;
422 let calc = SupplyCalculator::new(
423 mainnet_id(),
424 before_tge,
425 parse_ether("100").unwrap(),
426 parse_ether("100").unwrap(),
427 None,
428 );
429 assert_eq!(calc.circulating_supply(), U256::ZERO);
430 }
431
432 #[test]
433 fn test_locked_saturates_when_initial_less_than_unlocked() {
434 let now = post_tge_time();
436 let small_initial = parse_ether("100").unwrap();
437 let calc = SupplyCalculator::new(mainnet_id(), now, small_initial, small_initial, None);
438 assert_eq!(calc.locked(), U256::ZERO);
439 assert_eq!(calc.circulating_supply(), small_initial);
441 }
442}