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<String>> {
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 clone_box(&self) -> Box<dyn ProtocolSim> {
449 Box::new(self.clone())
450 }
451
452 fn as_any(&self) -> &dyn std::any::Any {
453 self
454 }
455
456 fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
457 self
458 }
459
460 fn eq(&self, other: &dyn ProtocolSim) -> bool {
461 if let Some(other_state) = other.as_any().downcast_ref::<Self>() {
462 self == other_state
463 } else {
464 false
465 }
466 }
467}
468
469#[cfg(test)]
470mod tests {
471 use super::*;
472
473 fn u256_dec(value: &str) -> U256 {
474 U256::from_str_radix(value, 10).expect("valid base-10 U256")
475 }
476
477 fn sample_state() -> EtherfiState {
478 EtherfiState {
479 block_timestamp: 1_764_901_727,
480 total_value_out_of_lp: u256_dec("2649956291248983147816190"),
481 total_value_in_lp: u256_dec("35878437939234433682319"),
482 total_shares: u256_dec("2479957712837255941780080"),
483 eth_amount_locked_for_withdrawl: Some(u256_dec("5572247384784800483589")),
484 liquidity_pool_native_balance: Some(u256_dec("35878437939234433682319")),
485 eth_redemption_info: Some(RedemptionInfo {
486 limit: BucketLimit {
487 capacity: 2_000_000_000,
488 remaining: 1_998_993_391,
489 last_refill: 1_764_901_727,
490 refill_rate: 23_148,
491 },
492 exit_fee_split_to_treasury_in_bps: 1000,
493 exit_fee_in_bps: 30,
494 low_watermark_in_bps_of_tvl: 100,
495 }),
496 }
497 }
498
499 #[test]
500 fn bucket_limit_from_u256_parses_fields() {
501 let capacity = 2_000_000_000u64;
502 let remaining = 1_999_995_000u64;
503 let last_refill = 1_767_694_523u64;
504 let refill_rate = 23_148u64;
505
506 let value = U256::from(capacity) |
507 (U256::from(remaining) << 64u32) |
508 (U256::from(last_refill) << 128u32) |
509 (U256::from(refill_rate) << 192u32);
510
511 let limit = BucketLimit::from_u256(value);
512 assert_eq!(limit.capacity, capacity);
513 assert_eq!(limit.remaining, remaining);
514 assert_eq!(limit.last_refill, last_refill);
515 assert_eq!(limit.refill_rate, refill_rate);
516 }
517
518 #[test]
519 fn redemption_info_from_u256_parses_fields() {
520 let limit = BucketLimit { capacity: 1, remaining: 2, last_refill: 3, refill_rate: 4 };
521 let exit_fee_split = 1000u16;
522 let exit_fee = 30u16;
523 let low_watermark = 100u16;
524
525 let value = U256::from(u64::from(exit_fee_split)) |
526 (U256::from(u64::from(exit_fee)) << 16u32) |
527 (U256::from(u64::from(low_watermark)) << 32u32);
528
529 let info = RedemptionInfo::from_u256(limit, value);
530 assert_eq!(info.limit, limit);
531 assert_eq!(info.exit_fee_split_to_treasury_in_bps, exit_fee_split);
532 assert_eq!(info.exit_fee_in_bps, exit_fee);
533 assert_eq!(info.low_watermark_in_bps_of_tvl, low_watermark);
534 }
535
536 #[test]
537 fn convert_to_bucket_unit_rounds_up() {
538 let amount = U256::from(BUCKET_UNIT_SCALE - 1);
539 let bucket = convert_to_bucket_unit(amount, true).expect("bucket");
540 assert_eq!(bucket, 1);
541
542 let exact = U256::from(BUCKET_UNIT_SCALE * 2);
543 let bucket_exact = convert_to_bucket_unit(exact, true).expect("bucket");
544 assert_eq!(bucket_exact, 2);
545 }
546
547 #[test]
548 fn convert_to_bucket_unit_rounds_down() {
549 let amount = U256::from(BUCKET_UNIT_SCALE - 1);
550 let bucket = convert_to_bucket_unit(amount, false).expect("bucket");
551 assert_eq!(bucket, 0);
552
553 let exact = U256::from(BUCKET_UNIT_SCALE * 3);
554 let bucket_exact = convert_to_bucket_unit(exact, false).expect("bucket");
555 assert_eq!(bucket_exact, 3);
556 }
557
558 #[test]
559 fn convert_to_bucket_unit_rejects_large_amounts() {
560 let scale = U256::from(BUCKET_UNIT_SCALE);
561 let max_amount = U256::from(u64::MAX) * scale;
562 let err = convert_to_bucket_unit(max_amount, true).unwrap_err();
563 match err {
564 SimulationError::FatalError(msg) => {
565 assert!(msg.contains("Amount too large"));
566 }
567 _ => panic!("unexpected error type"),
568 }
569 }
570
571 #[test]
572 fn bucket_limit_refill_caps_at_capacity() {
573 let limit = BucketLimit { capacity: 10, remaining: 1, last_refill: 100, refill_rate: 5 };
574 let refilled = limit.refill(103);
575 assert_eq!(refilled.remaining, 10);
576 assert_eq!(refilled.last_refill, 103);
577 }
578
579 #[test]
580 fn bucket_limit_refill_noop_same_or_older_time() {
581 let limit = BucketLimit { capacity: 10, remaining: 4, last_refill: 100, refill_rate: 5 };
582 let same = limit.refill(100);
583 assert_eq!(same.remaining, 4);
584 assert_eq!(same.last_refill, 100);
585
586 let older = limit.refill(99);
587 assert_eq!(older.remaining, 4);
588 assert_eq!(older.last_refill, 100);
589 }
590
591 #[test]
592 fn get_limits_eeth_to_eth_caps_by_bucket_remaining() {
593 let state = sample_state();
594 let info = state
595 .eth_redemption_info
596 .expect("redemption info");
597 let limit = info.limit.refill(state.block_timestamp);
598 let expected_max_in = U256::from(limit.remaining) * U256::from(BUCKET_UNIT_SCALE);
599
600 let (max_in, max_out) = state
601 .get_limits(Bytes::from(EETH_ADDRESS), Bytes::from(ETH_ADDRESS))
602 .expect("limits");
603
604 assert_eq!(max_in, u256_to_biguint(expected_max_in));
605 let eeth_shares = state
606 .shares_for_amount(expected_max_in)
607 .expect("shares");
608 let net_shares = mul_div(
609 eeth_shares,
610 U256::from(BASIS_POINT_SCALE) - U256::from(info.exit_fee_in_bps),
611 U256::from(BASIS_POINT_SCALE),
612 )
613 .expect("mul_div");
614 let expected_out = state
615 .amount_for_share(net_shares)
616 .expect("amount");
617 assert_eq!(max_out, u256_to_biguint(expected_out));
618 }
619
620 #[test]
621 fn get_limits_eeth_to_eth_returns_liquid_amount_when_below_low_watermark() {
622 let mut state = sample_state();
623 let info = state
624 .eth_redemption_info
625 .expect("redemption info");
626 let total_pooled = state.total_value_in_lp + state.total_value_out_of_lp;
627 let low_watermark = mul_div(
628 total_pooled,
629 U256::from(info.low_watermark_in_bps_of_tvl),
630 U256::from(BASIS_POINT_SCALE),
631 )
632 .expect("low watermark");
633 let locked = state
634 .eth_amount_locked_for_withdrawl
635 .expect("locked");
636 state.liquidity_pool_native_balance = Some(locked + low_watermark - U256::ONE);
637
638 let (max_in, max_out) = state
639 .get_limits(Bytes::from(EETH_ADDRESS), Bytes::from(ETH_ADDRESS))
640 .expect("limits");
641
642 assert_eq!(max_in, u256_to_biguint(low_watermark - U256::ONE));
643 assert_eq!(max_out, BigUint::ZERO);
644 }
645
646 #[test]
647 fn get_limits_weeth_to_eeth_uses_total_shares() {
648 let state = sample_state();
649 let max_weeth = state
650 .shares_for_amount(state.total_shares)
651 .expect("shares");
652 let (max_in, max_out) = state
653 .get_limits(Bytes::from(WEETH_ADDRESS), Bytes::from(EETH_ADDRESS))
654 .expect("limits");
655
656 assert_eq!(max_in, u256_to_biguint(max_weeth));
657 assert_eq!(max_out, u256_to_biguint(state.total_shares));
658 }
659}