1use std::collections::HashMap;
2
3use alloy::primitives::U256;
4use hex_literal::hex;
5use num_bigint::{BigInt, BigUint};
6use num_traits::ToPrimitive;
7use serde::{Deserialize, Serialize};
8use tycho_common::{
9 dto::ProtocolStateDelta,
10 models::token::Token,
11 simulation::{
12 errors::{SimulationError, TransitionError},
13 protocol_sim::{Balances, GetAmountOutResult, ProtocolSim},
14 },
15 Bytes,
16};
17
18use crate::evm::protocol::{
19 u256_num::{biguint_to_u256, u256_to_biguint, u256_to_f64},
20 utils::solidity_math::mul_div,
21};
22
23pub const EETH_ADDRESS: [u8; 20] = hex!("35fA164735182de50811E8e2E824cFb9B6118ac2");
24pub const WEETH_ADDRESS: [u8; 20] = hex!("Cd5fE23C85820F7B72D0926FC9b05b43E359b7ee");
25pub const ETH_ADDRESS: [u8; 20] = hex!("EeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE");
26pub const BASIS_POINT_SCALE: u64 = 10000;
27pub const BUCKET_UNIT_SCALE: u64 = 1_000_000_000_000;
28
29#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
30pub struct EtherfiState {
31 block_timestamp: u64,
32 total_value_out_of_lp: U256,
33 total_value_in_lp: U256,
34 total_shares: U256,
35 eth_amount_locked_for_withdrawl: Option<U256>,
36 liquidity_pool_native_balance: Option<U256>,
37 eth_redemption_info: Option<RedemptionInfo>,
38}
39
40#[derive(Copy, Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize)]
41pub struct RedemptionInfo {
42 limit: BucketLimit,
43 exit_fee_split_to_treasury_in_bps: u16,
44 exit_fee_in_bps: u16,
45 low_watermark_in_bps_of_tvl: u16,
46}
47
48#[derive(Copy, Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize)]
49pub struct BucketLimit {
50 capacity: u64,
52 remaining: u64,
54 last_refill: u64,
56 refill_rate: u64,
58}
59
60impl EtherfiState {
61 pub fn new(
62 block_timestamp: u64,
63 total_value_out_of_lp: U256,
64 total_value_in_lp: U256,
65 total_shares: U256,
66 eth_amount_locked_for_withdrawl: Option<U256>,
67 eth_redemption_info: Option<RedemptionInfo>,
68 liquidity_pool_native_balance: Option<U256>,
69 ) -> Self {
70 EtherfiState {
71 block_timestamp,
72 total_value_out_of_lp,
73 total_value_in_lp,
74 total_shares,
75 eth_amount_locked_for_withdrawl,
76 liquidity_pool_native_balance,
77 eth_redemption_info,
78 }
79 }
80
81 fn require_redemption_info(&self) -> Result<RedemptionInfo, SimulationError> {
82 self.eth_redemption_info
83 .ok_or_else(|| SimulationError::FatalError("missing eth redemption info".to_string()))
84 }
85
86 fn require_liquidity_balance(&self) -> Result<U256, SimulationError> {
87 self.liquidity_pool_native_balance
88 .ok_or_else(|| {
89 SimulationError::FatalError("missing liquidity pool native balance".to_string())
90 })
91 }
92
93 fn require_eth_amount_locked_for_withdrawl(&self) -> Result<U256, SimulationError> {
94 self.eth_amount_locked_for_withdrawl
95 .ok_or_else(|| {
96 SimulationError::FatalError("missing eth amount locked for withdrawal".to_string())
97 })
98 }
99
100 fn shares_for_amount(&self, amount: U256) -> Result<U256, SimulationError> {
101 let total_pooled_ether = self.total_value_in_lp + self.total_value_out_of_lp;
102 if total_pooled_ether == U256::ZERO {
103 return Ok(U256::ZERO)
104 }
105 Ok(amount * self.total_shares / total_pooled_ether)
107 }
108
109 fn amount_for_share(&self, share: U256) -> Result<U256, SimulationError> {
110 if self.total_shares == U256::ZERO {
111 return Ok(U256::ZERO)
112 }
113 let total_pooled_ether = self.total_value_in_lp + self.total_value_out_of_lp;
115 Ok(share * total_pooled_ether / self.total_shares)
116 }
117
118 fn shares_for_withdrawal_amount(&self, amount: U256) -> Result<U256, SimulationError> {
119 let total_pooled_ether = self.total_value_in_lp + self.total_value_out_of_lp;
120 if total_pooled_ether == U256::ZERO {
121 return Ok(U256::ZERO)
122 }
123 let numerator = amount * self.total_shares;
124 Ok(numerator + total_pooled_ether - U256::ONE / total_pooled_ether)
125 }
126}
127
128impl BucketLimit {
129 pub(crate) fn from_u256(value: U256) -> Self {
130 let mask = U256::from(u64::MAX);
131 Self {
132 capacity: (value & mask).to::<u64>(),
133 remaining: ((value >> 64u32) & mask).to::<u64>(),
134 last_refill: ((value >> 128u32) & mask).to::<u64>(),
135 refill_rate: ((value >> 192u32) & mask).to::<u64>(),
136 }
137 }
138
139 fn refill(mut self, now: u64) -> Self {
141 if now <= self.last_refill {
142 return self;
143 }
144 let delta = now - self.last_refill;
145 let tokens = (delta as u128) * (self.refill_rate as u128);
146 let new_remaining = (self.remaining as u128) + tokens;
147 if new_remaining > self.capacity as u128 {
148 self.remaining = self.capacity;
149 } else {
150 self.remaining = new_remaining as u64;
151 }
152 self.last_refill = now;
153 self
154 }
155}
156
157fn convert_to_bucket_unit(amount: U256, rounding_up: bool) -> Result<u64, SimulationError> {
159 let scale = U256::from(BUCKET_UNIT_SCALE);
160 let max_amount = U256::from(u64::MAX) * scale;
161 if amount >= max_amount {
162 return Err(SimulationError::FatalError(
163 "EtherFiRedemptionManager: Amount too large".to_string(),
164 ));
165 }
166 let bucket = if rounding_up { (amount + scale - U256::ONE) / scale } else { amount / scale };
168 if bucket > U256::from(u64::MAX) {
169 return Err(SimulationError::FatalError(
170 "EtherFiRedemptionManager: Amount too large".to_string(),
171 ));
172 }
173 Ok(bucket.to::<u64>())
174}
175
176impl RedemptionInfo {
177 pub(crate) fn from_u256(limit: BucketLimit, value: U256) -> Self {
178 let mask = U256::from(u64::from(u16::MAX));
179 let exit_fee_split_to_treasury_in_bps = (value & mask).to::<u16>();
180 let exit_fee_in_bps = ((value >> 16u32) & mask).to::<u16>();
181 let low_watermark_in_bps_of_tvl = ((value >> 32u32) & mask).to::<u16>();
182 Self {
183 limit,
184 exit_fee_split_to_treasury_in_bps,
185 exit_fee_in_bps,
186 low_watermark_in_bps_of_tvl,
187 }
188 }
189}
190
191#[typetag::serde]
192impl ProtocolSim for EtherfiState {
193 fn fee(&self) -> f64 {
194 0 as f64
195 }
196
197 fn spot_price(&self, base: &Token, quote: &Token) -> Result<f64, SimulationError> {
198 let base_unit = U256::from(10).pow(U256::from(base.decimals));
199 let quote_unit = U256::from(10).pow(U256::from(quote.decimals));
200 let quote_unit_f64 = u256_to_f64(quote_unit)?;
201 let to_price = |amount_out: U256| -> Result<f64, SimulationError> {
202 Ok(u256_to_f64(amount_out)? / quote_unit_f64)
203 };
204
205 if base.address.as_ref() == EETH_ADDRESS && quote.address.as_ref() == WEETH_ADDRESS {
206 to_price(self.shares_for_amount(base_unit)?)
207 } else if base.address.as_ref() == WEETH_ADDRESS && quote.address.as_ref() == EETH_ADDRESS {
208 to_price(self.amount_for_share(base_unit)?)
209 } else if base.address.as_ref() == ETH_ADDRESS && quote.address.as_ref() == EETH_ADDRESS {
210 to_price(self.shares_for_amount(base_unit)?)
211 } else if base.address.as_ref() == EETH_ADDRESS && quote.address.as_ref() == ETH_ADDRESS {
212 to_price(self.amount_for_share(base_unit)?)
213 } else {
214 Err(SimulationError::FatalError("unsupported spot price".to_string()))
215 }
216 }
217
218 fn get_amount_out(
219 &self,
220 amount_in: BigUint,
221 token_in: &Token,
222 token_out: &Token,
223 ) -> Result<GetAmountOutResult, SimulationError> {
224 let mut new_state = self.clone();
225 let amount_in = biguint_to_u256(&amount_in);
226 if token_in.address.as_ref() == ETH_ADDRESS && token_out.address.as_ref() == EETH_ADDRESS {
227 let amount_out = self.shares_for_amount(amount_in)?;
229 new_state.total_shares += amount_out;
230 new_state.total_value_in_lp += amount_in;
231 return Ok(GetAmountOutResult::new(
232 u256_to_biguint(amount_out),
233 BigUint::from(46_886u32), Box::new(new_state),
235 ))
236 }
237
238 if token_in.address.as_ref() == EETH_ADDRESS && token_out.address.as_ref() == ETH_ADDRESS {
239 let liquidity_pool_native_balance = self.require_liquidity_balance()?;
241 let eth_amount_locked_for_withdrawl = self.require_eth_amount_locked_for_withdrawl()?;
242 let eth_redemption_info = self.require_redemption_info()?;
243 let liquid_eth_amount = liquidity_pool_native_balance - eth_amount_locked_for_withdrawl;
244 let low_watermark = mul_div(
245 self.total_value_in_lp + self.total_value_out_of_lp,
246 U256::from(eth_redemption_info.low_watermark_in_bps_of_tvl),
247 U256::from(BASIS_POINT_SCALE),
248 )?;
249 if liquid_eth_amount < low_watermark || liquid_eth_amount - low_watermark < amount_in {
250 return Err(SimulationError::FatalError("Exceeded total redeemable amount".into()))
251 } else {
252 let bucket_unit = convert_to_bucket_unit(amount_in, true)?;
254 let mut limit = eth_redemption_info
255 .limit
256 .refill(self.block_timestamp);
257 if limit.remaining < bucket_unit {
258 return Err(SimulationError::FatalError("Exceeded rate limit".into()))
259 }
260 limit.remaining -= bucket_unit;
261 limit.last_refill = self.block_timestamp;
262 let mut updated_info = eth_redemption_info;
263 updated_info.limit = limit;
264 new_state.eth_redemption_info = Some(updated_info);
265 }
266 let eeth_shares = self.shares_for_amount(amount_in)?;
267 let eth_amount_out = self.amount_for_share(mul_div(
268 eeth_shares,
269 U256::from(BASIS_POINT_SCALE) - U256::from(eth_redemption_info.exit_fee_in_bps),
270 U256::from(BASIS_POINT_SCALE),
271 )?)?;
272 new_state.total_value_in_lp -= eth_amount_out;
273 new_state.total_shares -= self.shares_for_withdrawal_amount(eth_amount_out)?;
274 new_state.liquidity_pool_native_balance =
275 Some(liquidity_pool_native_balance - eth_amount_out);
276 let amount_out = u256_to_biguint(eth_amount_out);
277 return Ok(GetAmountOutResult::new(
278 amount_out,
279 BigUint::from(151_676u32), Box::new(new_state),
282 ))
283 }
284
285 if token_in.address.as_ref() == EETH_ADDRESS && token_out.address.as_ref() == WEETH_ADDRESS
286 {
287 let amount_out = u256_to_biguint(self.shares_for_amount(amount_in)?);
289 return Ok(GetAmountOutResult::new(
290 amount_out,
291 BigUint::from(70_489u32), Box::new(new_state),
293 ))
294 }
295
296 if token_in.address.as_ref() == WEETH_ADDRESS && token_out.address.as_ref() == EETH_ADDRESS
297 {
298 let amount_out = u256_to_biguint(self.amount_for_share(amount_in)?);
300 return Ok(GetAmountOutResult::new(
301 amount_out,
302 BigUint::from(60_182u32), Box::new(new_state),
304 ))
305 }
306
307 Err(SimulationError::FatalError("unsupported swap".to_string()))
308 }
309
310 fn get_limits(
311 &self,
312 sell_token: Bytes,
313 buy_token: Bytes,
314 ) -> Result<(BigUint, BigUint), SimulationError> {
315 if sell_token.as_ref() == WEETH_ADDRESS && buy_token.as_ref() == EETH_ADDRESS {
316 let max_weeth_amount = self.shares_for_amount(self.total_shares)?;
317 let max_eeth_amount = self.total_shares;
318 return Ok((u256_to_biguint(max_weeth_amount), u256_to_biguint(max_eeth_amount)));
319 }
320
321 if sell_token.as_ref() == EETH_ADDRESS && buy_token.as_ref() == ETH_ADDRESS {
322 let liquidity_pool_native_balance = self.require_liquidity_balance()?;
323 let eth_amount_locked_for_withdrawl = self.require_eth_amount_locked_for_withdrawl()?;
324 let eth_redemption_info = self.require_redemption_info()?;
325 let liquid_eth_amount = liquidity_pool_native_balance - eth_amount_locked_for_withdrawl;
326 let low_watermark = mul_div(
327 self.total_value_in_lp + self.total_value_out_of_lp,
328 U256::from(eth_redemption_info.low_watermark_in_bps_of_tvl),
329 U256::from(BASIS_POINT_SCALE),
330 )?;
331 if liquid_eth_amount < low_watermark {
332 return Ok((u256_to_biguint(liquid_eth_amount), BigUint::ZERO));
333 }
334 let mut max_eeth_amount = self.total_value_in_lp + self.total_value_out_of_lp;
335 let limit = eth_redemption_info
336 .limit
337 .refill(self.block_timestamp);
338 let bucket_unit = convert_to_bucket_unit(max_eeth_amount, true)?;
340 if limit.remaining < bucket_unit {
341 max_eeth_amount = U256::from(limit.remaining) * U256::from(BUCKET_UNIT_SCALE);
342 }
343 let eeth_shares = self.shares_for_amount(max_eeth_amount)?;
344 let eth_amount_out = self.amount_for_share(mul_div(
345 eeth_shares,
346 U256::from(BASIS_POINT_SCALE) - U256::from(eth_redemption_info.exit_fee_in_bps),
347 U256::from(BASIS_POINT_SCALE),
348 )?)?;
349 return Ok((u256_to_biguint(max_eeth_amount), u256_to_biguint(eth_amount_out)));
350 }
351
352 if sell_token.as_ref() == EETH_ADDRESS && buy_token.as_ref() == WEETH_ADDRESS {
353 return Ok((u256_to_biguint(U256::MAX), u256_to_biguint(U256::MAX)));
354 }
355
356 if sell_token.as_ref() == ETH_ADDRESS && buy_token.as_ref() == EETH_ADDRESS {
357 return Ok((u256_to_biguint(U256::MAX), u256_to_biguint(U256::MAX)));
358 }
359
360 Err(SimulationError::FatalError("unsupported swap".to_string()))
361 }
362
363 fn delta_transition(
364 &mut self,
365 delta: ProtocolStateDelta,
366 _tokens: &HashMap<Bytes, Token>,
367 _balances: &Balances,
368 ) -> Result<(), TransitionError> {
369 if let Some(block_timestamp) = delta
370 .updated_attributes
371 .get("block_timestamp")
372 {
373 self.block_timestamp = BigInt::from_signed_bytes_be(block_timestamp)
374 .to_u64()
375 .unwrap();
376 }
377 if let Some(total_value_out_of_lp) = delta
378 .updated_attributes
379 .get("totalValueOutOfLp")
380 {
381 self.total_value_out_of_lp = U256::from_be_slice(total_value_out_of_lp);
382 }
383 if let Some(total_value_in_lp) = delta
384 .updated_attributes
385 .get("totalValueInLp")
386 {
387 self.total_value_in_lp = U256::from_be_slice(total_value_in_lp);
388 }
389 if let Some(total_shares) = delta
390 .updated_attributes
391 .get("totalShares")
392 {
393 self.total_shares = U256::from_be_slice(total_shares);
394 }
395 if let Some(eth_amount_locked_for_withdrawl) = delta
396 .updated_attributes
397 .get("ethAmountLockedForWithdrawl")
398 {
399 self.eth_amount_locked_for_withdrawl =
400 Some(U256::from_be_slice(eth_amount_locked_for_withdrawl));
401 }
402 if let Some(liquidity_pool_native_balance) = delta
403 .updated_attributes
404 .get("liquidity_pool_native_balance")
405 {
406 self.liquidity_pool_native_balance =
407 Some(U256::from_be_slice(liquidity_pool_native_balance));
408 }
409 let eth_bucket_limiter = delta
410 .updated_attributes
411 .get("ethBucketLimiter")
412 .map(|value| U256::from_be_slice(value));
413 let eth_redemption_info = delta
414 .updated_attributes
415 .get("ethRedemptionInfo")
416 .map(|value| U256::from_be_slice(value));
417 if eth_bucket_limiter.is_some() || eth_redemption_info.is_some() {
418 let existing = self
419 .eth_redemption_info
420 .unwrap_or_default();
421 let mut limit = existing.limit;
422 let mut exit_fee_split = existing.exit_fee_split_to_treasury_in_bps;
423 let mut exit_fee = existing.exit_fee_in_bps;
424 let mut low_watermark = existing.low_watermark_in_bps_of_tvl;
425
426 if let Some(eth_bucket_limiter) = eth_bucket_limiter {
427 limit = BucketLimit::from_u256(eth_bucket_limiter);
428 }
429 if let Some(eth_redemption_info) = eth_redemption_info {
430 let parsed = RedemptionInfo::from_u256(limit, eth_redemption_info);
431 limit = parsed.limit;
432 exit_fee_split = parsed.exit_fee_split_to_treasury_in_bps;
433 exit_fee = parsed.exit_fee_in_bps;
434 low_watermark = parsed.low_watermark_in_bps_of_tvl;
435 }
436
437 self.eth_redemption_info = Some(RedemptionInfo {
438 limit,
439 exit_fee_split_to_treasury_in_bps: exit_fee_split,
440 exit_fee_in_bps: exit_fee,
441 low_watermark_in_bps_of_tvl: low_watermark,
442 });
443 }
444
445 Ok(())
446 }
447
448 fn query_pool_swap(
449 &self,
450 params: &tycho_common::simulation::protocol_sim::QueryPoolSwapParams,
451 ) -> Result<tycho_common::simulation::protocol_sim::PoolSwap, SimulationError> {
452 crate::evm::query_pool_swap::query_pool_swap(self, params)
453 }
454
455 fn clone_box(&self) -> Box<dyn ProtocolSim> {
456 Box::new(self.clone())
457 }
458
459 fn as_any(&self) -> &dyn std::any::Any {
460 self
461 }
462
463 fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
464 self
465 }
466
467 fn eq(&self, other: &dyn ProtocolSim) -> bool {
468 if let Some(other_state) = other.as_any().downcast_ref::<Self>() {
469 self == other_state
470 } else {
471 false
472 }
473 }
474}
475
476#[cfg(test)]
477mod tests {
478 use super::*;
479
480 fn u256_dec(value: &str) -> U256 {
481 U256::from_str_radix(value, 10).expect("valid base-10 U256")
482 }
483
484 fn sample_state() -> EtherfiState {
485 EtherfiState {
486 block_timestamp: 1_764_901_727,
487 total_value_out_of_lp: u256_dec("2649956291248983147816190"),
488 total_value_in_lp: u256_dec("35878437939234433682319"),
489 total_shares: u256_dec("2479957712837255941780080"),
490 eth_amount_locked_for_withdrawl: Some(u256_dec("5572247384784800483589")),
491 liquidity_pool_native_balance: Some(u256_dec("35878437939234433682319")),
492 eth_redemption_info: Some(RedemptionInfo {
493 limit: BucketLimit {
494 capacity: 2_000_000_000,
495 remaining: 1_998_993_391,
496 last_refill: 1_764_901_727,
497 refill_rate: 23_148,
498 },
499 exit_fee_split_to_treasury_in_bps: 1000,
500 exit_fee_in_bps: 30,
501 low_watermark_in_bps_of_tvl: 100,
502 }),
503 }
504 }
505
506 #[test]
507 fn bucket_limit_from_u256_parses_fields() {
508 let capacity = 2_000_000_000u64;
509 let remaining = 1_999_995_000u64;
510 let last_refill = 1_767_694_523u64;
511 let refill_rate = 23_148u64;
512
513 let value = U256::from(capacity) |
514 (U256::from(remaining) << 64u32) |
515 (U256::from(last_refill) << 128u32) |
516 (U256::from(refill_rate) << 192u32);
517
518 let limit = BucketLimit::from_u256(value);
519 assert_eq!(limit.capacity, capacity);
520 assert_eq!(limit.remaining, remaining);
521 assert_eq!(limit.last_refill, last_refill);
522 assert_eq!(limit.refill_rate, refill_rate);
523 }
524
525 #[test]
526 fn redemption_info_from_u256_parses_fields() {
527 let limit = BucketLimit { capacity: 1, remaining: 2, last_refill: 3, refill_rate: 4 };
528 let exit_fee_split = 1000u16;
529 let exit_fee = 30u16;
530 let low_watermark = 100u16;
531
532 let value = U256::from(u64::from(exit_fee_split)) |
533 (U256::from(u64::from(exit_fee)) << 16u32) |
534 (U256::from(u64::from(low_watermark)) << 32u32);
535
536 let info = RedemptionInfo::from_u256(limit, value);
537 assert_eq!(info.limit, limit);
538 assert_eq!(info.exit_fee_split_to_treasury_in_bps, exit_fee_split);
539 assert_eq!(info.exit_fee_in_bps, exit_fee);
540 assert_eq!(info.low_watermark_in_bps_of_tvl, low_watermark);
541 }
542
543 #[test]
544 fn convert_to_bucket_unit_rounds_up() {
545 let amount = U256::from(BUCKET_UNIT_SCALE - 1);
546 let bucket = convert_to_bucket_unit(amount, true).expect("bucket");
547 assert_eq!(bucket, 1);
548
549 let exact = U256::from(BUCKET_UNIT_SCALE * 2);
550 let bucket_exact = convert_to_bucket_unit(exact, true).expect("bucket");
551 assert_eq!(bucket_exact, 2);
552 }
553
554 #[test]
555 fn convert_to_bucket_unit_rounds_down() {
556 let amount = U256::from(BUCKET_UNIT_SCALE - 1);
557 let bucket = convert_to_bucket_unit(amount, false).expect("bucket");
558 assert_eq!(bucket, 0);
559
560 let exact = U256::from(BUCKET_UNIT_SCALE * 3);
561 let bucket_exact = convert_to_bucket_unit(exact, false).expect("bucket");
562 assert_eq!(bucket_exact, 3);
563 }
564
565 #[test]
566 fn convert_to_bucket_unit_rejects_large_amounts() {
567 let scale = U256::from(BUCKET_UNIT_SCALE);
568 let max_amount = U256::from(u64::MAX) * scale;
569 let err = convert_to_bucket_unit(max_amount, true).unwrap_err();
570 match err {
571 SimulationError::FatalError(msg) => {
572 assert!(msg.contains("Amount too large"));
573 }
574 _ => panic!("unexpected error type"),
575 }
576 }
577
578 #[test]
579 fn bucket_limit_refill_caps_at_capacity() {
580 let limit = BucketLimit { capacity: 10, remaining: 1, last_refill: 100, refill_rate: 5 };
581 let refilled = limit.refill(103);
582 assert_eq!(refilled.remaining, 10);
583 assert_eq!(refilled.last_refill, 103);
584 }
585
586 #[test]
587 fn bucket_limit_refill_noop_same_or_older_time() {
588 let limit = BucketLimit { capacity: 10, remaining: 4, last_refill: 100, refill_rate: 5 };
589 let same = limit.refill(100);
590 assert_eq!(same.remaining, 4);
591 assert_eq!(same.last_refill, 100);
592
593 let older = limit.refill(99);
594 assert_eq!(older.remaining, 4);
595 assert_eq!(older.last_refill, 100);
596 }
597
598 #[test]
599 fn get_limits_eeth_to_eth_caps_by_bucket_remaining() {
600 let state = sample_state();
601 let info = state
602 .eth_redemption_info
603 .expect("redemption info");
604 let limit = info.limit.refill(state.block_timestamp);
605 let expected_max_in = U256::from(limit.remaining) * U256::from(BUCKET_UNIT_SCALE);
606
607 let (max_in, max_out) = state
608 .get_limits(Bytes::from(EETH_ADDRESS), Bytes::from(ETH_ADDRESS))
609 .expect("limits");
610
611 assert_eq!(max_in, u256_to_biguint(expected_max_in));
612 let eeth_shares = state
613 .shares_for_amount(expected_max_in)
614 .expect("shares");
615 let net_shares = mul_div(
616 eeth_shares,
617 U256::from(BASIS_POINT_SCALE) - U256::from(info.exit_fee_in_bps),
618 U256::from(BASIS_POINT_SCALE),
619 )
620 .expect("mul_div");
621 let expected_out = state
622 .amount_for_share(net_shares)
623 .expect("amount");
624 assert_eq!(max_out, u256_to_biguint(expected_out));
625 }
626
627 #[test]
628 fn get_limits_eeth_to_eth_returns_liquid_amount_when_below_low_watermark() {
629 let mut state = sample_state();
630 let info = state
631 .eth_redemption_info
632 .expect("redemption info");
633 let total_pooled = state.total_value_in_lp + state.total_value_out_of_lp;
634 let low_watermark = mul_div(
635 total_pooled,
636 U256::from(info.low_watermark_in_bps_of_tvl),
637 U256::from(BASIS_POINT_SCALE),
638 )
639 .expect("low watermark");
640 let locked = state
641 .eth_amount_locked_for_withdrawl
642 .expect("locked");
643 state.liquidity_pool_native_balance = Some(locked + low_watermark - U256::ONE);
644
645 let (max_in, max_out) = state
646 .get_limits(Bytes::from(EETH_ADDRESS), Bytes::from(ETH_ADDRESS))
647 .expect("limits");
648
649 assert_eq!(max_in, u256_to_biguint(low_watermark - U256::ONE));
650 assert_eq!(max_out, BigUint::ZERO);
651 }
652
653 #[test]
654 fn get_limits_weeth_to_eeth_uses_total_shares() {
655 let state = sample_state();
656 let max_weeth = state
657 .shares_for_amount(state.total_shares)
658 .expect("shares");
659 let (max_in, max_out) = state
660 .get_limits(Bytes::from(WEETH_ADDRESS), Bytes::from(EETH_ADDRESS))
661 .expect("limits");
662
663 assert_eq!(max_in, u256_to_biguint(max_weeth));
664 assert_eq!(max_out, u256_to_biguint(state.total_shares));
665 }
666}